Top.Mail.Ru
OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

Nginx SSI и подзапросы: сборка страниц из блоков с кэшированием

Практическое руководство по Nginx SSI и subrequest: сборка страницы из блоков, фрагментное кэширование, разделение гостей и авторизованных, безопасные internal-локейшены, грамотные TTL и защита от шторма кэша. Ускоряем ответы без переписывания приложения.
Nginx SSI и подзапросы: сборка страниц из блоков с кэшированием

Серверные включения (SSI) в Nginx — недооценённый инструмент, который позволяет собирать итоговую HTML‑страницу из независимых блоков и запрашивать эти блоки параллельно как подзапросы. В связке с прокси‑кэшем это превращается в удобный «конструктор» производительности: тяжёлые, но редко меняющиеся части страницы можно кэшировать на уровне Nginx, а динамику оставлять приложению. Ниже — практическая методика, безопасные шаблоны и тонкости конфигурации, чтобы выжать максимум производительности без усложнения кода приложения.

Что такое SSI и как они работают в Nginx

SSI (Server Side Includes) — это простой язык директив, встроенный в ответ HTML, который Nginx обрабатывает фильтром перед отдачей клиенту. Ключевая возможность — директива include, которая инициирует подзапрос к URI на этом же Nginx и подставляет его вывод в место вызова. Для вас это означает: можно хранить страницу‑шаблон как «каркас», а меню, шапку, подвал, панель пользователя, виджеты — получать отдельными subrequest и кэшировать их по своим правилам.

Важно понимать, что подзапрос в контексте Nginx — это не отдельное HTTP‑соединение от браузера. Это внутренний запрос (internal) внутри воркера Nginx, который может проксироваться к апстриму (PHP‑FPM через FastCGI, приложению за proxy_pass и т.д.) или извлекаться из кэша. Подзапросы лёгкие, полностью неблокирующие и выполняются параллельно.

Где SSI уместны

SSI отлично подходят, когда:

  • страница состоит из независимых блоков с разной динамикой (шапка, навигация, авторизационный блок, рекомендации, подвал);
  • вы хотите кэшировать статичные и «псевдодинамические» фрагменты без кэширования всей страницы;
  • нужно уменьшить TTFB для тяжёлых участков, отдавая их из кэша Nginx;
  • приложение исторически монолитно, но есть потребность вынести часть нагрузки на уровень веб‑сервера;
  • надо распараллелить получение блоков (subrequest запускаются одновременно).

SSI — это не замена шаблонизатора в приложении. Это способ догнать производительность там, где переписать код быстро невозможно, и при этом чётко контролировать кэш на уровне фронтенд‑сервера.

Архитектура страницы с SSI и поток подзапросов

Включаем SSI: базовая конфигурация

SSI включаются в location, который отдаёт итоговую HTML‑страницу. Минимальный набор:

server {
    listen 80;
    server_name example.com;

    # Итоговая страница собирается с SSI
    location / {
        ssi on;
        ssi_types text/html;  # по умолчанию text/html, можно расширить
        # Если бэкенд сжимает ответы, а вы хотите фильтровать их SSI
        gunzip on;            # модуль gunzip должен быть доступен

        proxy_pass http://app;
        proxy_set_header Host $host;
    }
}

Модуль SSI не умеет разбирать уже сжатый контент. Вариантов два: отключить gzip на бэкенде для страниц с SSI или включить в Nginx директиву gunzip on, чтобы сервер мог распаковать проксируемый ответ, обработать SSI и снова сжать (если надо). Это критическая мелочь: без неё SSI просто не сработает.

Синтаксис SSI: включаем блоки

В самом HTML‑каркасе страницы вставляете SSI‑директивы. Для подзапроса используйте include virtual — он создаёт внутренний HTTP‑запрос к указанному URI (будет применён весь конфиг локации) и встроит ответ:

<!-- каркас HTML -->
<div class="layout">
  <header>
    <!--# include virtual="/_frag/header" -->
  </header>
  <nav>
    <!--# include virtual="/_frag/menu" -->
  </nav>
  <main>
    <!-- контент приложения -->
  </main>
  <aside>
    <!--# include virtual="/_frag/reco" -->
  </aside>
  <footer>
    <!--# include virtual="/_frag/footer" -->
  </footer>
</div>

Для чтения локальных файлов существует include file, но на проде предпочтительнее virtual, чтобы обрабатывать блоки через Nginx‑кэш и апстримы.

Дизайн подзапросов: internal‑локейшены для фрагментов

Хорошая практика — обрабатывать фрагменты в специальном префиксе, который недоступен извне. Тогда эти URI можно вызывать только как подзапросы из SSI, а не напрямую из браузера. Простой шаблон:

http {
    proxy_cache_path /var/cache/nginx/frag levels=1:2 keys_zone=frag_cache:50m max_size=1g inactive=30m use_temp_path=off;

    # Сегментация кэша гостей/авторизованных
    map $cookie_session $user_segment {
        default guest;
        ~.+ auth;
    }

    # Условие для обхода кэша при наличии приватных куки
    map $http_cookie $bypass_cache {
        default 0;
        ~*session= 1;
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            ssi on;
            ssi_types text/html;
            gunzip on;
            proxy_pass http://app;
            proxy_set_header Host $host;
        }

        # Фрагменты доступны только как подзапросы
        location /_frag/ {
            internal;
            proxy_pass http://app;

            proxy_cache frag_cache;
            proxy_cache_valid 200 5m;
            proxy_cache_lock on;
            proxy_cache_lock_timeout 10s;
            proxy_cache_background_update on;
            proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;

            # Ключ кэша с сегментацией по типу пользователя
            proxy_cache_key "$scheme|$proxy_host|$request_uri|$user_segment";

            # Обход кэша для приватных ответов
            proxy_no_cache $bypass_cache;
            proxy_cache_bypass $bypass_cache;

            proxy_set_header Host $host;
        }
    }
}

Здесь есть несколько важных моментов:

  • internal закрывает прямой доступ к фрагментам; браузер получит 403, что корректно. Только SSI внутри Nginx может запрашивать эти URI.
  • Ключ кэша добавляет $user_segment, чтобы разнести блоки для гостей и авторизованных. Это грубая, но простая сегментация (две корзины вместо миллионов вариаций). Если блок вообще не должен кэшироваться для авторизованных, используйте proxy_no_cache и proxy_cache_bypass по наличию приватных куки.
  • proxy_cache_lock и proxy_cache_background_update устраняют «шторм» кэша и не блокируют клиентские ответы при истечении TTL.

Кэширование подзапросов: TTL, валидация, заголовки

Стратегию TTL стоит выбирать отдельно для каждого блока. Например, меню можно кэшировать 30 минут, рекомендации — 1 минуту, подвал — сутки. Для этого удобно использовать разные локации или location ~ ^/_frag/(reco|menu|footer)$ с разными proxy_cache_valid.

Если бэкенд проставляет корректные Cache-Control, включите проверку валидности через proxy_cache_revalidate on, чтобы Nginx при истечении TTL пытался сделать условный запрос (If-Modified-Since/ETag) и экономил трафик и CPU. Это особенно полезно для больших фрагментов, которые меняются редко.

Помните, что для внутренних фрагментов не нужен Vary — их никто не запрашивает снаружи. Но если блок может быть запрошен напрямую (например, выделили его в публичный ресурс), корректно укажите Vary: Cookie или иные факторы вариативности, либо запретите прямой доступ через internal.

Настройка proxy_cache для кэширования фрагментов в Nginx

Производительность и масштабирование

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

  • Держите число SSI‑блоков разумным. 3–8 фрагментов обычно безболезненны. Десятки — мониторьте бэкенд и коннекты.
  • Обязательно включайте keepalive к апстриму (proxy_pass) и увеличьте keepalive в апстрим‑блоке, чтобы не плодить соединения.
  • Давайте более длинные TTL для тяжёлых и стабильных блоков, короткие — для легковесных и динамичных.
  • Используйте proxy_cache_lock, чтобы один клиент «прогревал» фрагмент, а остальные получали старую копию (use_stale updating).
  • Включите логирование статуса кэша в access‑лог, чтобы видеть попадания/промахи по фрагментам.
log_format main '$remote_addr - $request $status $body_bytes_sent '
                 'cache:$upstream_cache_status rt:$request_time urt:$upstream_response_time';
access_log /var/log/nginx/access.log main;
FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Смежные материалы: посмотрите про оптимизацию изображений WebP/AVIF через map в Nginx в статье «Оптимизация изображений WebP/AVIF через map и кэш» (WebP/AVIF через map) и про уровни кеша приложения в «Memcached и Redis для PHP» (Memcached/Redis для PHP).

Типовые шаблоны сборки

1) Каркас с кэшируемыми «псевдостатическими» блоками

Шапка, меню, подвал хранятся в кэше минутами/часами. Основной контент приходит от приложения без кэша. Это резко снижает нагрузку на бэкенд и ускоряет TTFB на пике.

2) Гостевой/авторизованный сегменты

Гостям показываем кэш, авторизованным — кэш ограниченно или вовсе его обход. Для этого достаточно двух корзин кэша через $user_segment. Если авторизованных много и блок персонализирован — лучше не кэшировать и отдавать напрямую, иначе получится «взрыв» вариативности кэша.

3) «Горячие» блоки с микрокэшем

Некоторые динамические блоки можно кэшировать буквально 1–5 секунд, сглаживая пики. Подобная микрокэш‑стратегия на фрагментах отлично переживает всплески трафика и защищает базу данных от шторма.

Безопасность: запреты и гигиена заголовков

Несколько правил безопасности при использовании SSI:

  • Все фрагменты — только за internal. Это защищает от обхода и перечисления.
  • Не прокидывайте лишние заголовки клиента в подзапросы. Строго ограничьте список требуемых proxy_set_header. Особенно аккуратнее с Cookie и Authorization.
  • Не используйте пользовательский ввод напрямую в SSI‑директивах. Если передаёте параметры из URI в подзапросы, валидируйте их на стороне приложения или через map/if с whitelisting.
  • Контролируйте размер значений SSI через ssi_value_length, если используете длинные переменные в директивах SSI.

Совместимость с сжатием и фильтрами

SSI — это фильтр, который должен видеть исходный текст HTML. Если бэкенд прислал gzip, модуль SSI не сможет распарсить ответ. Варианты:

  • Отключить gzip на бэкенде для HTML с SSI. Сжимать уже на Nginx после обработки SSI.
  • Включить gunzip on в Nginx, чтобы сервер прозрачным образом распаковал ответ, применил SSI и снова сжал.

Также проверьте список типов, для которых включён SSI: по умолчанию text/html. Если шаблон отдаётся как application/xhtml+xml или text/xml, добавьте эти типы в ssi_types.

Отладка и диагностика

Несколько трюков, чтобы убедиться, что всё работает как задумано:

  • Включите логирование $upstream_cache_status и смотрите, что происходит по URI /_frag/.... «HIT» — хорошо, «MISS» — прогрев, «BYPASS/EXPIRED/UPDATING» — тоже ожидаемо.
  • Временное логирование заголовков ответа фрагмента через add_header помогает понять, не конфликтуют ли заголовки кэша, нет ли Set-Cookie, который ломает шаринг.
  • Проверяйте, не делает ли приложение редиректы на фрагменты (это ломает подзапросы). Для фрагментов отдавайте 200 с нужной разметкой без редиректов.

Расширенные настройки кэш‑слоя для фрагментов

Если используете лимиты запросов, определите зону в контексте http:

limit_req_zone $binary_remote_addr zone=fragburst:10m rate=10r/s;

Если блоки тяжёлые и популярные, используйте дополнительные предохраняющие настройки:

location /_frag/reco {
    internal;
    proxy_pass http://app;

    proxy_cache frag_cache;
    proxy_cache_key "$scheme|$proxy_host|$request_uri|$user_segment";

    proxy_cache_valid 200 1m;
    proxy_cache_lock on;
    proxy_cache_lock_age 20s;
    proxy_cache_lock_timeout 10s;

    proxy_cache_background_update on;
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;

    # Защита от перегрева бэкенда
    limit_req zone=fragburst burst=20 nodelay;
}

Лимиты запросов к фрагментам особенно полезны при инцидентах: страница может запускать несколько подзапросов, и даже небольшой всплеск трафика превращается в лавину обращений к бэкенду. Мягкий троттлинг и ста́лый кэш позволяют пережить пики.

Ошибки и крайние случаи

  • «SSI не работают» — проверьте, что ответ не сжат до SSI, и что в локации итоговой страницы реально включено ssi on. Убедитесь, что контент‑тайп входит в ssi_types.
  • «Падает производительность» — вы, вероятно, сделали слишком много фрагментов или слишком короткий TTL. Начните с 3–5 блоков и TTL в 1–5 минут для «тяжёлых», остальное без кэша.
  • «В кэше оказываются приватные данные» — проверьте proxy_no_cache/proxy_cache_bypass по кукам и заголовкам. Лучше вообще не кэшировать персонализированный блок, если вариативность высока.
  • «Фрагмент редиректит» — не делайте редиректов в подзапросах; отдавайте прямо фрагмент с 200 OK. Редирект в subrequest не подставится как HTML.

Ограничения и ожидания

SSI — это простой, но мощный инструмент. У него нет логики уровня полноценного шаблонизатора, а вложенные SSI и сложные условия лучше не использовать. Держите правила простыми: include нужных блоков, минимум логики в SSI, максимум — в приложении и конфиге Nginx. В обмен вы получаете параллельные подзапросы, гибкое фрагментное кэширование и предсказуемое поведение на уровне фронтенда.

Практический чеклист внедрения

  • Выделите 3–6 блоков страницы, которые логично кэшировать отдельно (header/menu/footer/reco/user‑panel).
  • Соберите каркас страницы и вставьте SSI include virtual для каждого блока.
  • Создайте /_frag/ локации с internal и настройте proxy_cache с разумными TTL и защитой от шторма.
  • Сегментируйте гостей/авторизованных через map и добавьте сегмент в ключ кэша или полностью отключите кэш для персональных блоков.
  • Включите gunzip on и проверьте ssi_types.
  • В лог добавьте $upstream_cache_status и проверьте долю HIT после прогрева.

Итоги

Связка «nginx + ssi + subrequest + кэш» — быстрый способ ускорить выдачу, разгрузить приложение и базу, а также повысить устойчивость на пиках. Она особенно органична на VDS: вы контролируете конфигурацию, дисковое пространство под кэш и сетевые лимиты. Начните с минимального набора фрагментов, настройте кэш‑слой и наблюдайте за метриками — вы увидите, как снижается время ответа и нагрузка на бэкенд. Главное — держать архитектуру простой: небольшое число блоков, предсказуемые TTL, аккуратная сегментация и жёсткое правило «internal для всего, что связано с SSI».

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

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

Мониторинг OPcache: метрики, алерты и быстрая диагностика утечек OpenAI Статья написана AI Fastfox

Мониторинг OPcache: метрики, алерты и быстрая диагностика утечек

OPcache ускоряет PHP, но под нагрузкой может «захлебнуться»: заканчивается память, растёт фрагментация, падает hit rate. Разбираем ...
rclone для больших файлов: multipart‑загрузки, параллелизм и контроль памяти OpenAI Статья написана AI Fastfox

rclone для больших файлов: multipart‑загрузки, параллелизм и контроль памяти

Большие файлы и S3 требуют точной настройки rclone: multipart‑загрузка, параллелизм потоков, контроль памяти и полосы, устойчивост ...
Maintenance mode правильно: 503, Retry‑After и кастомные страницы OpenAI Статья написана AI Fastfox

Maintenance mode правильно: 503, Retry‑After и кастомные страницы

Плановые работы не должны ломать индексацию и кэш, а пользователи — видеть понятную страницу. Разбираем правильный maintenance: ко ...