Подводные камни при использовании кэширования в nginx

    В web-сервер и reverse-proxy nginx встроены очень мощные возможности по кэшированию HTTP-ответов. Однако в ряде случаев документации и примеров не хватает, в результате не все получается так легко и просто, как хотелось бы. Например, мои конфиги nginx-а местами написаны кровью. Этой статьей я попробую немного улучшить ситуацию.

    В этой статье: а) подводные камни при полностраничном кэшировании; б) кэширование с ротацией; в) создание динамического «окна» в закэшированной странице.

    Я буду предполагать, что вы используете связку nginx+fastcgi_php. Если вы применяете nginx+apache+mod_php, просто замените имена директив с fastcgi_cache* на proxy_cache*

    Если выбирать, кэшировать ли страницу на стороне PHP или на стороне nginx, я выбираю nginx. Во-первых, это позволяет отдавать 5-10 тыс. запросов в секунду без каких-либо сложностей и без умных разговоров о «высокой нагрузке». Во-вторых, nginx самостоятельно следит за размером кэша и чистит его как при устаревании, так и при вытеснении нечасто используемых данных.

    Кэширование всей страницы целиком


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

    Например, закэшировать главную страницу можно так:

    fastcgi_cache_path /var/cache/nginx levels= keys_zone=wholepage:50m;
    ...
    server {
      ...
      location / {
        ...
        fastcgi_pass 127.0.0.1:9000;
        ...
        # Включаем кэширование и тщательно выбираем ключ кэша.
        fastcgi_cache wholepage;
        fastcgi_cache_valid 200 301 302 304 5m;
        fastcgi_cache_key "$request_method|$http_if_modified_since|$http_if_none_match|$host|$request_uri";
        # Гарантируем, что разные пользователи не получат одну и ту же сессионную Cookie.
        fastcgi_hide_header "Set-Cookie";
        # Заставляем nginx кэшировать страницу в любом случае, независимо от
        # заголовков кэширования, выставляемых в PHP.
        fastcgi_ignore_headers "Cache-Control" "Expires";
      }
    }
    

    Я не сильно преувеличу, если скажу, что каждая строчка в этом конфиге написана кровью. Здесь много подводных камней, давайте их все рассмотрим.

    fastcgi_cache_path: простота отладки тоже важна

    fastcgi_cache_path /var/cache/nginx levels= keys_zone=wholepage:50m;

    В директиве fastcgi_cache_path я выставляю «пустое» значение для levels. Хотя это немного снижает производительность (файлы будут напрямую создаваться в /var/cache/nginx, без разбиения по директориям), но зато на порядок облегчает отладку и диагностику проблем с кэшем. Поверьте, вам еще не раз придется руками залезать в /var/cache/nginx и смотреть, что там хранится.

    fastcgi_cache_valid: кэшируем код ответа 304 тоже

    fastcgi_cache_valid 200 301 302 304 5m;

    В директиве fastcgi_cache_valid мы заставляем кэшировать не только стандартные коды 200 ОК, 301 Moved Permanently и 302 Found, но также и 304 Not Modified. Почему? Давайте вспомним, что означает 304. Он выдается с пустым телом ответа в двух случаях:
    • Если браузер послал заголовок «If-Modified-Since: date», в котором date больше либо равна значению заголовка ответа «Last-Modified: date». Т.е. клиент спрашивает: «Есть ли новая версия с момента date? Если нет, верни мне 304 и сэкономь трафик. Если есть, отдай мне тело страницы».
    • Если браузер послал заголовок «If-None-Match: hash», где hash совапдает со значением заголовка ответа «ETag: hash». Т.е. клиент спрашивает: «Отличается ли текущая версия страницы от той, что я запросил в прошлый раз? Если нет, верни мне 304 и сэкономь трафик. Если да, отдай тело страницы».
    В обоих случаях Last-Modified или ETag будут взяты, скорее всего, из кэша nginx, и проверка пройдет очень быстро. Нам незачем «дергать» PHP только для того, чтобы скрипт выдал эти заголовки, особенно в свете того, что клиентам, которым уйдет ответ 200, он будет отдан из кэша.

    fastcgi_cache_key: внимательно работаем с зависимостями

    fastcgi_cache_key "$request_method|$http_if_modified_since|$http_if_none_match|$host|$request_uri";

    Особого внимания заслуживает значение в директиве fastcgi_cache_key. Я привел минимальное рабочее значение этой директивы. Шаг вправо, шаг влево, и вы начнете в ряде случаев получать «неправильные» данные из кэша. Итак:
    • Зависимость от $request_method нам нужна, т.к. HEAD-запросы в Интернете довольно часты. Ответ на HEAD-запрос никогда не содержит тела. Если убрать зависимость от $request_method, то может так совпасть, что кто-то до вас запросил главную страницу HEAD-методом, а вам потом по GET отдастся пустой контент.
    • Зависимость от $http_if_modified_since нужна для того, чтобы кэш с ответом 304 Not Modified не был случайно отдан клиенту, делающему обычный GET-запрос. Иначе клиент может получить пустой ответ из кэша.
    • То же самое и с $http_if_none_match. Мы должны быть застрахованы от выдачи пустых страниц клиентам!
    • Наконец, зависимость от $host и $request_uri не требует комментариев.
    fastcgi_hide_header: решаем проблемы с безопасностью

    fastcgi_hide_header «Set-Cookie»;

    Директива fastcgi_hide_header очень важна. Без нее вы серьезно рискуете безопасностью: пользователи могут получить чужие сессии через сессионную Cookie в кэше. (Правда, в последних версиях nginx что-то было сделано в сторону автоматического учета данного фактора.) Понимаете, как это происходит? На сайт зашел Вася Пупкин, ему выдалась сессия и сессионная Cookie. Пусть кэш на тот момент оказался пустым, и в него записалась Васина Cookie. Затем пришел другой пользователь, получил ответ из кэша, а в нем — и Cookie Васи. А значит, и его сессию тоже.

    Можно, конечно, сказать: давайте не будем вызывать session_start() на главной странице, тогда и с Cookies проблем не будет. В теории это так, но на практике данный способ очень неустойчив. Сессии часто стартуют «отложено», и достаточно какой-либо части кода «случайно» вызвать функцию, требующую доступа к сессии, как мы получим дыру в безопасности. А безопасность — такая штука, что если в той или иной методике может возникнуть дыра по неосторожности, то эта методика считается «дырявой» по определению. К тому же есть и другие Cookies, кроме сессионной; их тоже не надо записывать в кэш.

    fastcgi_ignore_headers: не даем сайту «лечь» от нагрузки при опечатке

    fastcgi_ignore_headers «Cache-Control» «Expires»;

    Сервер nginx обращает внимание на заголовки Cache-Control, Expires и Pragma, которые выдает PHP. Если в них сказано, что страницу не нужно кэшировать (либо что она уже устарела), то nginx не записывает ее в кэш-файл. Это поведение, хотя и кажется логичным, на практике порождает массу сложностей. Поэтому мы его блокируем: благодаря fastcgi_ignore_headers в кэш-файлы попадет содержимое любой страницы, независимо от ее заголовков.

    Что же это за сложности? Они опять связаны с сессиями и функцией session_start(), которая в PHP по умолчанию выставляет заголовки «Cache-Control: no-cache» и «Pragma: no-cache». Здесь существует три решения проблемы:
    • Не пользоваться session_start() на странице, где предполагается кэширование. Один из минусов этого способа мы уже рассмотрели выше: достаточно одного неосторожного движения, и ваш сайт, принимающий тысячи запросов в секунду на закэшированную главную страницу, моментально «ляжет», когда кэш отключится. Второй минус — нам придется управлять логикой кэширования в двух местах: в конфиге nginx и в PHP-коде. Т.е. эта логика окажется «размазанной» по совершенно разным частям системы.
    • Выставить ini_set('session.cache_limiter', ''). Это заставит PHP запретить вывод каких-либо заголовков, ограничивающих кэширование при работе с сессиями. Проблема здесь та же: «размазанность» логики кэширования, ведь в идеале мы бы хотели, чтобы все кэширование управлялось из единого места.
    • Игнорировать заголовки запрета кэширования при записи в кэш-файлы при помощи fastcgi_ignore_headers. Кажется, это беспроигрышное решение, поэтому я его и советую.

    Кэширование с ротацией


    Статическая главная страница — это не так уж и интересно. Что делать, если на сайте много материалов, а Главная выступает в роли своеобразной «витрины» для них? На такой «витрине» удобно отображать «случайные» материалы, чтобы разные пользователи видели разное (и даже один пользователь получал новый контент, перезагрузив страницу в браузере).

    Решение задачи — кэширование с ротацией:
    1. Мы заставляем скрипт честно выдавать элементы главной странице в случайном порядке, выполняя необходимые запросы в базу данных (пусть это и медленно).
    2. Затем мы сохраняем в кэше не одну, а, скажем, 10 вариантов страницы.
    3. Когда пользователь заходит на сайт, мы показываем ему один из этих вариантов. При этом, если кэш пуст, то запускается скрипт, а если нет, то результат возвращается из кэша.
    4. Устанавливаем время устаревания кэша малым (например, 1 минута), чтобы за день разные пользователи «отсмотрели» все материалы сайта.
    В итоге первые 10 запросов к скрипту-генератору выполнятся «честно» и «нагрузят» сервер. Зато потом они «осядут» в кэше и в течение минуты будут выдаваться уже быстро. Прирост производительности тем больше, чем больше посетителей на сайте.

    Вот кусочек конфига nginx, реализующий кэширование с ротацией:

    fastcgi_cache_path /var/cache/nginx levels= keys_zone=wholepage:50m;
    perl_set $rand 'sub { return int rand 10 }';
    ...
    server {
      ...
      location / {
        ...
        fastcgi_pass 127.0.0.1:9000;
        ...
        # Включаем кэширование и тщательно выбираем ключ кэша.
        fastcgi_cache wholepage;
        fastcgi_cache_valid 200 301 302 304 1m;
        fastcgi_cache_key "$rand|$request_method|$http_if_modified_since|$http_if_none_match|$host|$request_uri";
        # Гарантируем, что разные пользователи не получат одну и ту же сессионную Cookie.
        fastcgi_hide_header "Set-Cookie";
        # Заставляем nginx кэшировать страницу в любом случае, независимо от
        # заголовков кэширования, выставляемых в PHP.
        fastcgi_ignore_headers "Cache-Control" "Expires";
    
        # Заставляем браузер каждый раз перезагружать страницу (для ротации).
        fastcgi_hide_header "Cache-Control";
        add_header Cache-Control "no-store, no-cache, must-revalidate, post-check=0, pre-check=0";
        fastcgi_hide_header "Pragma";
        add_header Pragma "no-cache";
    
        # Выдаем всегда свежий Last-Modified.
        expires -1; # Внимание!!! Эта строка expires необходима!
        add_header Last-Modified $sent_http_Expires;
      }
    }
    

    Вы можете заметить, что по сравнению с предыдущим примером мне пришлось добавить еще 6 директив в location. Они все очень важные! Но не будем забегать вперед, рассмотрим все по порядку.

    perl_set: зависимость-рандомизатор

    perl_set $rand 'sub { return int rand 10 }';

    С директивой perl_set все просто. Мы создаем переменную, при использовании которой nginx будет вызывать функцию встроенного в него Perl-интерпретатора. По словам автора nginx, это достаточно быстрая операция, так что мы не будем «экономить на спичках». Переменная принимает случайное значение от 0 до 9 в каждом из HTTP-запросов.

    fastcgi_cache_key: зависимость от рандомизатора

    fastcgi_cache_key "$rand|$request_method|...";

    Теперь мы замешиваем переменную-рандомизатор в ключ кэша. В итоге получается 10 разных кэшей на один и тот же URL, что нам и требовалось. Благодаря тому, что скрипт, вызываемый при кэш-промахе, выдает элементы главной страницы в случайном порядке, мы получаем 10 разновидностей главной страницы, каждая из которой «живет» 1 минуту (см. fastcgi_cache_valid).

    add_header: принудительно выключаем браузерный кэш

    fastcgi_hide_header "Cache-Control";
    add_header Cache-Control "no-store, no-cache, must-revalidate, post-check=0, pre-check=0";
    fastcgi_hide_header "Pragma";
    add_header Pragma "no-cache";
    

    Выше мы говорили, что nginx чувствителен к кэш-заголовкам, выдаваемым PHP-скриптом. Если PHP-скрипт возвращает заголовки «Pragma: no-cache» или «Cache-Control: no-store» (а также еще некоторые, например, «Cache-Control: не-сохранять, не-выдавать, меня-тут-не-было, я-этого-не-говорил, чья-это-шляпа»), то nginx не будет сохранять результат в кэш-файлах. Специально чтобы подавить такое его поведение, мы используем fastcgi_ignore_headers (см. выше).

    Чем отличается «Pragma: no-cache» от «Cache-Control: no-cache»? Только тем, что Pragma — наследие HTTP/1.0 и сейчас поддерживается для совместимости со старыми браузерами. В HTTP/1.1 используется Cache-Control.

    Однако есть еще кэш в браузере. И в некоторых случаях браузер может даже не пытаться делать запрос на сервер, чтобы отобразить страницу; вместо этого он достанет ее из собственного кэша. Т.к. у нас ротация, нам такое поведение неудобно: ведь каждый раз, заходя на страницу, пользователь должен видеть новые данные. (На самом деле, если вы все же хотите закэшировать какой-нибудь один вариант, то можно поэкспериментировать с заголовком Cache-Control.)

    Директива add_header как раз и передает в браузер заголовок запрета кэширования. Ну а чтобы этот заголовок случайно не размножился, мы вначале убираем из HTTP-ответа то, что записал туда PHP-скрипт (и то, что записалось в nginx-кэш): директива fastcgi_hide_header. Ведь вы, когда пишете конфиг nginx-а, не знаете, что там надумает выводить PHP (а если используется session_start(), то он точно надумает). Вдруг он выставит свой собственный заголовок Cache-Control? Тогда их будет два: PHP-шный и добавленный нами через add_header.

    expires и Last-Modified: гарантируем перезагрузку страницы

    expires -1; # Внимание!!! Эта строка expires необходима!
    add_header Last-Modified $sent_http_Expires;
    

    Еще один трюк: мы должны выставить Last-Modified равным текущему времени. К сожалению, в nginx нет переменной, хранящей текущее время, однако она магическим образом появляется, если указать директиву expires -1.

    Хотя это сейчас (октябрь 2009 г.) не задокументировано, nginx создает переменные вида $sent_http_XXX для каждого заголовка ответа XXX, отданного клиенту. Одной из них мы и пользуемся.

    Почему же так важно выставлять текущим временем этот заголовок? Все довольно просто.
    1. Давайте представим, что PHP выдал заголовок «Last-Modified: некоторая_дата».
    2. Данный заголовок будет записан в кэш-файл nginx (можете проверить: в нашем примере файлы хранятся в /var/cache/nginx), а потом отдан в браузер клиенту.
    3. Браузер запомнит страницу и дату ее модификации...
    4. … поэтому при следующем заходе пользователя на сайт в HTTP-запросе будет заголовок-вопрос «If-Modified-Since: некоторая_дата».
    5. Что же сделает nginx? Он достанет страницу из своего кэша, разберет ее заголовки и сравнит Last-Modified с If-Modified-Since. Если значения совпадут (или первое окажется меньше второго), то nginx вернет ответ «304 Not Modified» с пустым телом. И пользователь не увидит никакой ротации: он получит то, что уже видел раньше.
    На самом деле, большой вопрос, как поведет себя браузер при наличии одновременно Last-Modified и Cache-Control no-cache. Будет ли он делать запрос If-Modified-Since? Кажется, что разные браузеры ведут тут себя по-разному. Экспериментируйте.

    Есть и еще один повод выставлять Last-Modified вручную. Дело в том, что PHP-функция session_start() принудительно выдает заголовок Last-Modified, но указывает в нем… время изменения PHP-файла, который первый получил управление. Следовательно, если у вас на сайте все запросы идут на один и тот же скрипт (Front Controller), то ваша Last-Modified будет почти всегда равна времени изменения этого единственного скрипта, что совершенно не верно.

    Динамическое «окно» в закэшированной странице


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

    В ту часть страницы, которая должна быть динамической, вставьте вот такой «HTML-комментарий»:

    <!--# include virtual="/get_user_info/" -->
    

    С точки зрения кэша nginx данный комментарий — обычный текст. Он будет сохранен в кэш-файле именно в виде комментария. Однако позже, при прочтения кэша, сработает модуль SSI nginx, который обратится к динамическому URL. Конечно, по адресу /get_user_info/ должен быть PHP-обработчик, который выдает содержимое данного блока. Более подробно данный способ описан в этой статье с Хабра.

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

    ssi on;
    

    Директива SSI include имеет еще одно, крайне важное свойство. Когда на странице встречаются несколько таких директив, то все они начинают обрабатываться одновременно, в параллельном режиме. Так что, если у вас на странице 4 блока, каждый из которых загружается 200мс, в сумме страница будет получена пользователем через 200мс, а не через 800.

    Исходный текст данной статьи можно прочитать тут: http://dklab.ru/chicken/nablas/56.html
    Поделиться публикацией
    Комментарии 83
      –4
      хабракат...(
        +2
        Сорри, добавил. Тут в хабраредакторе сломался предпросмотр: жуть как неудобно статью форматировать. Вместо превью выдается что-то типа <redirect_url>http://habrahabr.ru/blogs/hi/72539/</redirect_urlok. Вот и приходится «на живом» править.
          –2
          ужас) а что за браузер?
            0
            Firefox. А при чем тут браузер? Это прямо выдача в HTML такая. Видимо, забыли шаблон наложить на XML, либо что-то подобное…
            0
            Да, есть такое. Пришлось даже временно IE7 расчехлить.
          0
          Что делать со страницами, которые полностью динамические? Как принудительно отключить для них кеширование и нужно ли это делать?

          Как бы вы посоветовали сделать так, чтобы ключ кеширования можно было бы устанавливать в самом PHP? К примеру, кешировать страницу в зависимости от группы пользователей?
            –1
            Мне кажется что тут уже дело даже не в жинксе, можно средствами пхп делать тот же кеш и им управлять, добавился комент — переписали кеш если это возможно, инструментов очень много и все зависит от типа сайта. И не бойтесь пользоваться кешированием через пхп, я понимаю что это ресурсозатратно, но доступ к базе и отдача осуществляется намного дольше. А харды у нас сегодня дешевле процов. Если же сайт «очень динамичен» и очень много пользователей а решение нужно с серверной стороны, то думаю что поставив кеш даже на 30 секунд будет видно спад нагрузки. В основном время от прочтения топика и коментирования проходит не меньше.
              +2
              Кэш PHP на уровне ob_start() — это, конечно, лучше, чем отсутствие полностраничного кэша. Однако все равно потери будут порядка 5-10 мс на вызов, в то время как в случае nginx они практически нулевые.

              Что касается
              > Как бы вы посоветовали сделать так, чтобы ключ кеширования можно было бы устанавливать в самом PHP?
              то использовать кэш nginx в таком режиме не получится: ведь чтобы определить ключ, вам потребуется запустить PHP-скрипт, а это уже промах мимо кэша по определению. Вся суть nginx-кэширования в том, чтобы не допускать ряд запросов вообще до PHP.

              Я рекомендую всю логику кэширования прописывать в едином месте: либо в конфиге nginx (и хранить его в системе контроля версий, конечно же), либо — в PHP (но тогда nginx-кэширование не используется). А чтобы для разных групп юзеров кэши были разные, установите текущую группу в куку $group, а потом замешайте $cookie_group (переменная такая в nginx создается для этой куки) в ключ кэширования. Это один из вариантов.
                0
                > в то время как в случае nginx они практически нулевые.
                а разве nginx не блокируется на файловом IO?
                0
                Имелись ввиду страницы, содержимое которых генерируется в зависимости от того, какой пользователь залогинен.
                  0
                  Респект Диме за статью, ее прочитал еще в рассылке на Алехе.

                  я тут сейчас модуль с маемкешом заканчиваю…
                  вот только не надо, что изобретаю велосипед… модуль нужен для записи в мемкеш (типа REST).
                  так вот, о чем я…

                  он в 80 раз быстрее работает, чем то-же самое ранее делали через пхп.
                0
                Я чего-то не пойму,
                  0
                  keys_zone=wholepage:50m;

                  Тут задается время кеширования (50m)?
                    +3
                    Это мегабайты, размер shared memory для еще более быстрого доступа к кэшу. Документация рулит.
                      0
                      Если точнее — размер кеша ключей в shared memory
                      0
                      Время хранения задается в той же строчке примерно так:
                      fastcgi_cache_path /tmp/nginx/ levels=1:2 keys_zone=fastcgi_cache:16m max_size=256m inactive=1d;
                        0
                        Это именно время хранения, а не время валидности. Цитата из документации:

                          0
                          Кроме того, все активные ключи и информация о данных хранятся в разделяемой памяти — зоне, имя и размер которой задаётся параметром keys_zone. Если к данным кэша не обращются в течение времени, заданного параметром inactive, то данные удаляются, независимо от их свежести. По умолчанию inactive равен 10 минутам.
                    0
                    Мда, кеширование на уровне nginx конечно очень призводительно, но по моему, так это как-то криво, ведь страницы кешируются безусловно, без учета мнения приложения, в обход его логики. И вообще, как-то коряво имхо.

                    Правильнее, наверно, все же, в приложении отдавать правильные http-заголовки, а nginx пусть их интерпретирует :)
                      0
                      > ведь страницы кешируются безусловно, без учета мнения приложения, в обход его логики
                      Это не совсем так. В nginx неплохие возможности по анализу параметров, кук и т.д.

                      > Правильнее, наверно, все же, в приложении отдавать правильные http-заголовки, а nginx пусть их интерпретирует :)
                      Так далеко не всегда получается, в статье частично обосновывается, почему. Штука в том, что до приложения дело не доходит при кэш-попадании. Соответственно, никто, кроме nginx, и не может решить, валиден кэш или нет.
                        0
                        Да, кстати, хочу подчеркнуть, что полностраничное nginx-кэширование — это не панацея, это просто один из инструментов, который иногда очень хорошо подходит. Большинство кэширования в реальных приложениях, наверное, располагается между слоем модели и слоем доступа к БД. Но это совсем-совсем другое. Здесь же речь идет только о полностраничном кэшировании.
                        • НЛО прилетело и опубликовало эту надпись здесь
                            0
                            Не столько PHP уродлив, сколько программисты непредсказуемы. Карандаш на острие стьит, но недолго; лучше его перевернуть и поставить устойчиво. Отсюда и совет.
                            • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                Вы так уверенно говорите… Хочу спросить, была ли у Вас практика в этом вопросе именно применительно к nginx?
                                • НЛО прилетело и опубликовало эту надпись здесь
                                    0
                                    ОК, значит, у нас есть альтернативное мнение. Может, потом как-нибудь появится и альтернативная статья… это всегда интересно.
                                  0
                                  если бы по невнимательности программиста все вылилось действительно лишь в незакешированную страницу — то да.
                          0
                          Допустим для главной страницы мы настроили кеширование. И логика «описывать правила кеширования в одном месте» призывает нас настроить кеширование и для всех прочих страниц. А как быть в случае когда этих страниц очень много и требуются очень отличающиеся правила.

                          Можно разделить условно страницы, которые очень часто обновляются и очень редко, а также существует некая золотая середина.

                          Соответственно получается такая картина:

                          Вариант1: Мы оптимизируем выдачу часто-обновляемых страниц. При этом получаем дополнительную нагрузку на редко-обновляемые страницы (то есть они будут дергаться в любом случае раз в секунду, так как PHP уже не может сказать, что страница не менялась, так?).

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

                          Я так понимаю, что логичным выходом из этого будет индивидуальный подход к разным разделам и даже конкретным страницам. Но не создаст ли это еще большие сложности с тем, что надо будет индивидуально затачивать кеш огромного числа страниц (а потом добавится какой-нибудь баннер и придется снова менять логику кеширования), а конфиг nginx разрастется до безумных пределов (и соответственно сложность управления им сильно возрастет)?
                            0
                            написание конфигов можно автоматизировать :)
                              0
                              Если предполагается хороший выигрыш, то можно так поступить. А иначе это лишняя работа :-)
                              0
                              Когда в приложении огромное количество стратегий для полностраничного кэширования, ИМХО стоит подумать, а нельзя ли их все свести к одному-двум. На моей практике именно так происходило: в одном проекте было вообще всего одно правило для всех страниц (все страницы для Гостя кэшировались), в другом — два правила (одно для склеенных CSS+JS, другое — для группы страниц с высокой нагрузкой). Полностраничное кэширование — это не кэширование блоков/выборок из БД, оно обычно сильно проще и гораздо менее универсально.
                                0
                                Кеширование для гостя это интересно… Это не на webo.in случаем? :-)

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

                                а вот подход к кешированию действительно нужен комплекксный: где надо логику отдаем энджиниксу,
                                а где нужно -приложению. Где нужно кешируем блок и собираем через SSI
                                а где надо — через шаблонизатор.
                                0
                                Отличная статься, огромное спасибо.

                                Можно задавать Last-Modified явно в конфиге и обновлять по крону раз в сутки или реже/чаще.
                                Конфиг с этой директивой можно инклюдить, для простоты.
                                  0
                                  Важно, чтобы Last-Modified менялся каждую секунду, т.е. реально содержал текущее время. Иначе сервер будет отдавать Not Modified при рефреше страницы, что в случае ротации не очень хорошо. Впрочем, можно поэкспериментировать с Cache-Control: no-cache, no-store, must-revalidate. Возможно, тогда с Last-Modified и не понадобятся манипуляции, я пока не проверял во всех браузерах, как они реагируют.
                                  0
                                  Планируется ли еще что-нибудь вкусное про nginx выложить?)
                                    0
                                    да, статья хорошая. Но все же хорошо было бы озвучить общий подход к серверному кэшированию. Если это — только один из инструментов, то каковы критерии применения остальных? Когда мы можем перекинуть расчет валидности кэша на фронтенд, когда — на бэкенд, а когда — вообще не кэшировать, либо кэшировать отдельные блоки.

                                    Да, по поводу4 блоков по 200мс — это сильно :) Особенно, если число ядер у бэкенда меньше, чем 4 — потоки будут изумительно параллелиться :)
                                      +1
                                      Бэкендов же много. Не обязательно на один все запросы пойдут, они распределятся по кластеру.
                                      0
                                      Спасибо!
                                        0
                                        отличная статья
                                          0
                                          Правильно ли я понял первую часть статьи в этом месте:

                                          # Гарантируем, что разные пользователи не получат одну и ту же сессионную Cookie.
                                          fastcgi_hide_header «Set-Cookie»;

                                          Сценарий — заходит Пользователь1, кеш пустой, запрос обрабатывается php, который ставит куку с идентификатором сессии, nginx кладет ответ в кеш и прячет куку.

                                          Заходит Пользователь2. Если я правильно понял, он получит данные из кеша, но при этом не получит куки с идентификатором сессии?
                                            0
                                            практически правильно, только один нюанс — в закешированной странице кука останется, а прячется она при отдаче ответа.

                                            а для индентификатора сессии всегда можно использовать встроенное средство nginx — модуль userid
                                              0
                                              все таки не очень понятно, а если у меня допустим не session id в куке а какие то полезные данные приложения (допустим, настройки языка)?

                                              1. сохранятся ли они в кеш?
                                              2. каким образом они будут обработаны для второго пользователя?
                                                0
                                                Эта директива блокирует выдачу в браузер ЛЮБЫХ команд установки кук. Но, заметьте, только УСТАНОВКИ. Сами куки, уже установленные где-то в другой части сайта, никуда не денутся (потому что они управляются браузером, сервер их не ставит, а только читает).

                                                Да, в кэш-файле куки все останутся. Туда вообще попадет ровно то, что выдал PHP, без изменений (и, кажется, на это нельзя повлиять). Можно вырезать только при отдаче.
                                                  0
                                                  Значит, если скрипт, ответ которого кешируется, ставит куки, а nginx их режет, данный способ не подходит. Нужно либо отдельно обрабатывать установку кук, в обход данного механизма кеширования, либо писать куки из скрипта вообще.

                                                  Спасибо!
                                                    0
                                                    *либо НЕ писать куки из скрипта вообще.
                                                  0
                                                  nginx пишет в файл кеша весь документ, который получает, включая заголовки (т.е. все куки, которые вы передаете клиенту сохранятся). Что с ними делать дальше — это уж как вам нравится — прячьте куки, оставляйте. Только один кукис спрятать, а другой показать не получится (использование модуля sub для этих целей рассматривать не будем:) ).

                                                  Если вы прячете куки — то они не будут отданы и первому пользователю.
                                              0
                                              Дмитрий можно вопрос?
                                              У меня главная — индивидуальна для каждого зареганого пользователя и статична для всех анонимов.

                                              Т.е. посетитель N1 видит одно, а посетитель 2 — совсем другое.

                                              Второй нюанс, для пользователей заведены псевдосубдомены. Насколько я понял nginx «их не видит», и когда пользователь заходит, например на страницу recoilme.mp3tter.com/ nginx послает его на индексную страницу, в которой уже определяется принадлежность соответствующему субдомену.

                                              Третье — пользователь загрузил файл (оставил сообщение), при обновлении надо отразить с последними изменениями главную…
                                              Кругом жопа…

                                              Я отчаялся реализовать кэш в данных условиях. Просто насколько я понял Вы тоже боролись с твиттер-лайк сайтом, может быть подскажете реализуемо ли это в принципе нгинксом? Или на уровне приложения реализовывали? В каком примерно направлении копать?
                                              Заранее спасибо.
                                                0
                                                В таком сайте лучше всего хранить главную страницу в memcache и отдавать ее отуда. Если изменяется — просто перерисовываем в памяти.
                                                  0
                                                  Не выйдет мне кажется. Главная для всех разная. Более того, recoilme.mp3tter.com для рекойлми != recoilme.mp3tter.com для другого посетителя.
                                                  Я в отчаянии. Единственное что наверно можно придумать тут — кешировать запросы. Но и тут жопа. Сиквуль заточен для операций над пулом записей, а для кеша эфективней порезать запросы по одной записи и потом собирать результаты в массив. Как это разрулил twitter — вообще непонятно…
                                                  Либо я чего то недопонимаю…
                                                    0
                                                    у меня появилась такая идея — кладем данные для хозяина поддомена в мемкеш с ключем, скажем ид его сессии + хост, при заходе на страницу пользователями проверяем, наличие такого ключа, если нет — идем в именованный локейшн из которого проверяем наличие ключа «гость + хост», если нет — лезем в еще один именованный локейшн, который лезет в пхп. Единственное — я не помню, можно ли переопределять error_page внутри именованного локейшена.

                                                    слегка сумбурно, но может чем поможет :)
                                                      0
                                                      Почему невыйдет?
                                                      Для анонимных она одинаковая, верно?
                                                      А для зарегенных можно подготавливать статику на диск и отдавать ее напрямую
                                                    0
                                                    По первому пункту — нгинкс можно научить смотреть есть ли сессия (допустим, что у неавторизованных её нет) и если да, то передавать управление пхп, если нет — отдать кеш.
                                                      +1
                                                      Да, кстати, я применял такую технику выключения кэша для залогина. Надо будет дописать это в статье.
                                                      0
                                                      Если у вас контент страницы зависит от некоего user_id, то засуньте тогда значение этого user_id в fastcgi_cache_key — тогда будут кешироваться разные варианты страницы в зависимости от cache_key.
                                                        0
                                                        спасибо, попробую!
                                                          0
                                                          ой, а откуда нгинксу взять юзерайди?
                                                          Он умеет читать куки?
                                                            0
                                                            понял, надо для залогиненных создавать сессию.
                                                              +2
                                                              Умеет. $cookie_имяВашейКуки
                                                          0
                                                          Спасибо за статью, как раз подумываю переложить кеширование с Catalyst::Plugin::PageCache на nginx, руки не доходили проверить, будет ли кешироваться страница с обработанными SSI-директивами, или без изменений. Было бы замечательно, если:
                                                          1. Можно было бы кешировать страницу только для неавторизованных пользователей. Описанный в документации способ fastcgi_cache_key "...$cookie_user";, не подходит — нужно кешировать только в случае пустой куки, а не для каждого её значения. Хотя, можно попробовать загнать директиву fastcgi_cache в if.
                                                          2. Была бы возможность принудительно очищать закешированную страницу по запросу из приложения. Надо проверить, будет ли работать просто удаление файла с именем, равным md5 от fastcgi_cache_key.
                                                            +1
                                                            По пункту 1 — там, кажется, решение тоже нетривиальное. Допишу скоро.
                                                            По пункту 2 — лучше не делайте так, используйте sysoev.ru/nginx/docs/http/ngx_http_memcached_module.html — он гораздо удобнее, если речь заходит об очистке кэша из приложения.
                                                              0
                                                              Эх, тогда, видимо, придется оставить использование Catalyst::Plugin::PageCache — абсолютно прозрачно для приложения и можно быстро сменить кеширующий бэкенд с файлов на тот же мемкеш или memory-mapped файлы. Слишком уж негибкое получается кеширование nginx'ом, хоть, наверное, и быстрей на порядок, но мне пока и кеша на уровне приложения хватает.
                                                            +2
                                                            Хотелось-бы увидеть пример отключения кэша при наличие определённой куки (отключение кэша для зарегеных) (реализация сейчас заставляет юзать X-Accel-Expires в приложении)
                                                              +1
                                                              Примерно вот так:

                                                              location ~ ^(здесь_урлы_которые_нужно_кэшировать)$ {
                                                                  set $test_cache_on "$cookie_debugMode|$arg_nocache|любые_другие_значения_непустые_для_отключения_кэша";
                                                                  if ($test_cache_on = "") {
                                                                      rewrite .* /php_cache last;
                                                                  }
                                                                  rewrite ^ /php_no_cache last;
                                                              }
                                                              
                                                              location /php_cache {
                                                                  internal;
                                                                  ...
                                                                  fastcgi_cache wholepage;
                                                                  ...
                                                              }
                                                              

                                                              Я допишу в статье попозже.
                                                                +1
                                                                Чертовски хочется
                                                                if($cookie_userid) fastcgi_cache off;

                                                                :)
                                                                  0
                                                                  set $tocache 1;
                                                                  if ($http_cookie ~* "sessionId=([^;]+)(?:;|$)") {
                                                                  set $tocache 0;
                                                                  }
                                                                  ...
                                                                  if ($tocache) {
                                                                  fastcgi_pass ...
                                                                  }

                                                                  примерно так
                                                                    0
                                                                    наверное, вместо set $tocache 0 можно поставить и fastcgi_cache off, хотя я не гуру настроек нгинкса.
                                                                      0
                                                                      Директива fastcgi_cache не может находиться внутри блока if. Так что это не сработает.
                                                                        0
                                                                        fastcgi_cache не может быть в if
                                                                        0
                                                                        А в чём смысл этой конструкции? fastcgi_pass для всех должен быть один.
                                                                          0
                                                                          У меня немного для других целей, не для fastcgi_cache. Кеш ровно такой же (по логике) пишет приложение, а нгинкс при наличии соотв. файла (if -f) и отсутствию сесии отдаёт кеш. Я полагал, что можно приспособить слегка такой подход под описываемое автором.
                                                                        0
                                                                        Я приводил мой вариант несколько дней назад
                                                                  0
                                                                  А при кэшировании nginx+memcache есть какой-нибудь аналог директиве fastcgi_hide_header «Set-Cookie»?
                                                                    0
                                                                    а она там не нужна, в мемкеш вы кладете сами все, что вам захочется
                                                                    0
                                                                    а как быть с SE оптимищацией и Last-Modified? гугл насколько мне известно смотрит на этот заголовок.
                                                                      0
                                                                      В статье есть предложение поэкспериментировать. Возможно, если указать более жесткий Cache-Control, то не потребуется удалять Last-Modified.

                                                                      Нужно только учитывать, что PHP выставляет Last-Modified при вызова session_start(). Причем выставляет он его равным… дате изменения файла PHP, который запустился в результате запроса. В большинстве случаев это какой-нибудь index.php из FrontController, который вообще не меняется. Соответственно, те, кто использует сессии, чаще всего имеют неправильный Last-Modified.
                                                                        0
                                                                        ну не все используют пхп ;)
                                                                      +1
                                                                      Судя по всему, приведенные примеры настроек примерно наполовину представляет из себя летопись хождения по граблям — как говорит сам автор, «каждая строчка писана кровью».

                                                                      Сначала программист, не подозревающий о Cache-Control, наступает на первые грабли и случайно отключает кеширование главной страницы. Автор предполагает, что лучшее решение это запрет Cache-Control на корню, но в результате наступает на несколько очередных грабель — во-первых, из-за отсутствия Cache-Control начинают кешироваться Cookie, а во-вторых, из-за отрезанных Expires страницы начинают плохо кешироваться в броузерах, которые в результате шлют слишком много запросов с If-Modified-Since. С первым справляемся отрезанием самих Cookie, а со вторым — кешированием ответов 304 Not Modified. Напоследок же автор пытается эмулировать Cache-Control через отрезание Last-Modified, что уже напрямую идет вразрез RFC на HTTP/1.1 и скорее всего, чревато очередными граблями.

                                                                      В итоге же все это комсомольское путешествие оказывается всего-лишь следствием одного из возможных решений специфической проблемы автора и совсем не обязательно разламывать управление кешированием HTTP из-за того, что какой-то программист однажды накосячил. Так что рекомендую воспринимать эту статью с хорошей долей grain of salt и думать самостоятельно о последствиях отключения чего-либо.
                                                                        0
                                                                        По поводу истории — не угадали. :-) Были совсем другие грабли. Но насчет «самостоятельного думания» — Вы совершенно правы.
                                                                          0
                                                                          fastcgi_cache_key "$request_method|$http_if_modified_since|$http_if_none_match|$host|$request_uri"; — это полный пэ. ладно бы оно было только на личном сайте автора, а так и на хабру попало, и по всему рунету расползлось… рекомендация по поводу восприятия этой статьи — лучшее что есть на этой странице вообще…
                                                                          0
                                                                          Кстати, в рассылку nginx предложили элегантное решение для Last-Modified. Оно работает, проверил. Я изменил статью соответствующим образом.

                                                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                                          Самое читаемое