Высокая производительность

индекс
187,32

Подводные камни при использовании кэширования в 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
16 октября 2009, 01:50
265

комментарии (83)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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
alfa #
Чертовски хочется
if($cookie_userid) fastcgi_cache off;

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

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

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

Сначала программист, не подозревающий о 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
DmitryKoterov #
По поводу истории — не угадали. :-) Были совсем другие грабли. Но насчет «самостоятельного думания» — Вы совершенно правы.
0
config #
fastcgi_cache_key "$request_method|$http_if_modified_since|$http_if_none_match|$host|$request_uri"; — это полный пэ. ладно бы оно было только на личном сайте автора, а так и на хабру попало, и по всему рунету расползлось… рекомендация по поводу восприятия этой статьи — лучшее что есть на этой странице вообще…
0
DmitryKoterov #
Кстати, в рассылку nginx предложили элегантное решение для Last-Modified. Оно работает, проверил. Я изменил статью соответствующим образом.

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