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

Sky Cat skysecret at gmail.com
Fri Oct 16 06:07:46 MSD 2009


Большое спасибо. Очень интересная статья. Благодаря ей я сегодня ночью
настроил кэширование в Drupal. Если кому-то интересно, отпишусь позже о
результатах тестирования.

16 октября 2009 г. 3:41 пользователь Dmitry Koterov <dmitry at koterov.ru>написал:

> Я тут статью черканул: http://dklab.ru/chicken/nablas/56.html
> Если есть мысли/замечания/комментарии/уточнения, буду рад внести изменения.
>
>
> =======================
>
> Подводные камни при использовании кэширования в nginx  [15 октября 2009
> г.] обсудить статью в форуме<http://dklab.ru/redir?http://forum.dklab.ru/viewtopic.php?t=35526Z:%5Chome%5Cdklab%5Cwww%5Cchicken%5Cnablas%5C56.html>
>
>  В web-сервер и reverse-proxy nginx <http://sysoev.ru/nginx/docs/>встроены очень мощные возможности по кэшированию HTTP-ответов. Однако в ряде
> случаев документации и примеров не хватает, в результате не все получается
> так легко и просто, как хотелось бы. Этой статьей я попробую немного
> улучшить ситуацию.
>
>  [image: Лирическое отступление]
>
> *Я буду предполагать, что вы используете связку nginx+fastcgi_php. Если вы
> применяете nginx+apache+mod_php, просто замените имена директив с
> fastcgi_cache* на proxy_cache*. *
>
> Если выбирать, кэшировать ли страницу на стороне PHP или на стороне nginx,
> я выбираю nginx. Во-первых, это позволяет отдавать 5-10 тыс. запросов в
> секунду без каких-либо сложностей и без умных разговоров о "высокой
> нагрузке". Во-вторых, nginx самостоятельно следит за размером кэша и чистит
> его как при устаревании, так и при вытеснении нечасто используемых данных.
> Кэширование всей страницы целиком
>
> Если на вашем сайте главная страница хоть и генерируется динамически, но
> меняется достаточно редко, можно сильно снизить нагрузку на сервер,
> закэшировав ее в nginx. При высокой посещаемости даже кэширование на
> короткий срок (5 минут и меньше) уже дает огромный прирост в
> производительности, ведь кэш работает очень быстро. Даже закэшировав
> страницу всего на 30 секунд, вы все равно добьетесь значительной разгрузки
> сервера, сохранив при этом динамичность обновления данных (во многих случаях
> обновления раз в 30 секунд вполне достаточно).
>
> Например, закэшировать главную страницу можно так:
>
>   скопировать код в буфер обмена <http://dklab.ru/chicken/nablas/56.html#>
>  * Листинг 1 <http://dklab.ru/chicken/nablas/56.html#list1> *
>
> 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<http://sysoev.ru/nginx/docs/http/ngx_http_fastcgi_module.html#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<http://sysoev.ru/nginx/docs/http/ngx_http_fastcgi_module.html#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<http://sysoev.ru/nginx/docs/http/ngx_http_fastcgi_module.html#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<http://sysoev.ru/nginx/docs/http/ngx_http_fastcgi_module.html#fastcgi_hide_header>очень важна. Без нее вы серьезно рискуете безопасностью: пользователи могут
> получить чужие сессии через сессионную Cookie в кэше. (Правда, в последних
> версиях nginx что-то было сделано в сторону автоматического учета данного
> фактора.) Понимаете, как это происходит? На сайт зашел Вася Пупкин, ему
> выдалась сессия и сессионная Cookie. Пусть кэш на тот момент оказался
> пустым, и в него записалась Васина Cookie. Затем пришел другой пользователь,
> получил ответ из кэша, а в нем - и Cookie Васи. А значит, и его сессию тоже.
>
>
>  [image: Чайник]
>
> *Можно, конечно, сказать: давайте не будем вызывать session_start() на
> главной странице, тогда и с Cookies проблем не будет. В теории это так, но
> на практике данный способ очень неустойчив. Сессии часто стартуют
> "отложено", и достаточно какой-либо части кода "случайно" вызвать функцию,
> требующую доступа к сессии, как мы получим дыру в безопасности. А
> безопасность -- такая штука, что если в той или иной методике можетвозникнуть дыра по неосторожности, то эта методика считается "дырявой" по
> определению. К тому же есть и другие Cookies, кроме сессионной; их тоже не
> надо записывать в кэш. *
>
> fastcgi_ignore_headers: не даем сайту "лечь" от нагрузки при опечатке
>
> fastcgi_ignore_headers "Cache-Control" "Expires";
>
> Сервер nginx обращает внимание на заголовки Cache-Control, Expires и
> Pragma, которые выдает PHP. Если в них сказано, что страницу не нужно
> кэшировать (либо что она уже устарела), то nginx не записывает ее в
> кэш-файл. Это поведение, хотя и кажется логичным, на практике порождает
> массу сложностей. Поэтому мы его блокируем: благодаря
> fastcgi_ignore_headers<http://sysoev.ru/nginx/docs/http/ngx_http_fastcgi_module.html#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. Кажется, это беспроигрышное решение, поэтому
>    я его и советую.
>
>  [image: Чайник]
>
> * Думаете, это все аспекты кэширования?
> Ничего подобного, мы только на середине. *
>
> Кэширование с ротацией
>
> Статическая главная страница -- это не так уж и интересно. Что делать, если
> на сайте много материалов, а Главная выступает в роли своеобразной "витрины"
> для них? На такой "витрине" удобно отображать "случайные" материалы, чтобы
> разные пользователи видели разное (и даже один пользователь получал новый
> контент, перезагрузив страницу в браузере).
>
> Решение задачи -- *кэширование с ротацией*:
>
>    1. Мы заставляем скрипт честно выдавать элементы главной странице в
>    случайном порядке, выполняя необходимые запросы в базу данных (пусть это и
>    медленно).
>    2. Затем мы сохраняем в кэше не одну, а, скажем, 10 вариантов страницы.
>
>    3. Когда пользователь заходит на сайт, мы показываем ему один из этих
>    вариантов. При этом, если кэш пуст, то запускается скрипт, а если нет, то
>    результат возвращается из кэша.
>    4. Устанавливаем время устаревания кэша малым (например, 1 минута),
>    чтобы за день разные пользователи "отсмотрели" все материалы сайта.
>
> В итоге первые 10 запросов к скрипту-генератору выполнятся "честно" и
> "нагрузят" сервер. Зато потом они "осядут" в кэше и в течение минуты будут
> выдаваться уже быстро. Прирост производительности тем больше, чем больше
> посетителей на сайте.
>
> Вот кусочек конфига nginx, реализующий кэширование с ротацией:
>
>   скопировать код в буфер обмена <http://dklab.ru/chicken/nablas/56.html#>
>  * Листинг 2 <http://dklab.ru/chicken/nablas/56.html#list2> *
>
> 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-cache";
>     fastcgi_hide_header "Pragma";
>     add_header Pragma "no-cache";
>
>     # Никогда не выдаем браузеру заголовок Last-Modified.
>
>     fastcgi_hide_header "Last-Modified";
>     add_header Last-Modified "";
>   }
> }
>
>  Вы можете заметить, что по сравнению с предыдущим примером мне пришлось
> добавить еще 6 директив в location. Они тоже написаны кровью. Но не будем
> забегать вперед, рассмотрим все по порядку.
> perl_set: зависимость-рандомизатор
>
> perl_set $rand 'sub { return int rand 10 }';
>
> С директивой perl_set<http://sysoev.ru/nginx/docs/http/ngx_http_perl_module.html#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-cache";
> fastcgi_hide_header "Pragma";
> add_header Pragma "no-cache";
>
> Выше мы говорили, что nginx чувствителен к кэш-заголовкам, выдаваемым
> PHP-скриптом. Если PHP-скрипт возвращает заголовки "Pragma: no-cache" или
> "Cache-Control: no-cache" (а также еще некоторые), то nginx не будет
> сохранять результат в кэш-файлах. Специально чтобы подавить такое его
> поведение, мы используем fastcgi_ignore_headers (см. выше).
>
>  [image: Чайник]
>
> * Чем отличается "Pragma: no-cache" от "Cache-Control: no-cache"? Только
> тем, что Pragma -- наследие HTTP/1.0 и сейчас поддерживается для
> совместимости со старыми браузерами. В HTTP/1.1 используется Cache-Control.
> *
>
> Однако есть еще кэш в браузере. И в некоторых случаях браузер может даже не
> пытаться делать запрос на сервер, чтобы отобразить страницу; вместо этого он
> достанет ее из собственного кэша. Т.к. у нас ротация, нам такое поведение
> неудобно: ведь каждый раз, заходя на страницу, пользователь должен видеть
> новые данные. (На самом деле, если вы все же хотите закэшировать
> какой-нибудь один вариант, то можно поэкспериментировать с заголовком
> Cache-Control.)
>
> Директива add_header<http://sysoev.ru/nginx/docs/http/ngx_http_headers_module.html#add_header>как раз и передает в браузер заголовок запрета кэширования. Ну а чтобы этот
> заголовок случайно не размножился, мы вначале убираем из HTTP-ответа то, что
> записал туда PHP-скрипт (и то, что записалось в nginx-кэш): директива
> fastcgi_hide_header<http://sysoev.ru/nginx/docs/http/ngx_http_fastcgi_module.html>.
> Ведь вы, когда пишете конфиг nginx-а, не знаете, что там надумает выводить
> PHP (а если используется session_start(), то он точно надумает). Вдруг он
> выставит свой собственный заголовок Cache-Control? Тогда их будет два:
> PHP-шный и добавленный нами через add_header.
> add_header Last-Modified: гарантируем перезагрузку страницы
>
> fastcgi_hide_header "Last-Modified";
> add_header Last-Modified "";
>
> Еще один трюк: мы должны исключить из HTTP-ответа заголовок Last-Modified,
> либо же выставить его равным текущему времени. (К сожалению, nginx пока не
> поддерживает установку Last-Modified в текущее время без использования
> встроенного Perl, поэтому мы просто удаляем заголовок.) Гарантировано
> исключить заголовок можно директивой add_header Last-Modified "". Но на
> всякий случай (а также для симметрии) я еще добавил сюда
> fastcgi_hide_header, он не помешает.
>
> Почему же так важно удалять (или выставлять текущим временем) этот
> заголовок? Все довольно просто.
>
>    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" с пустым телом. И пользователь не увидит никакой ротации: он
>    получит то, что уже видел раньше.
>
>  [image: Лирическое отступление]
>
> *На самом деле, большой вопрос, как поведет себя браузер при наличии
> одновременно Last-Modified и Cache-Control no-cache. Будет ли он делать
> запрос If-Modified-Since? Кажется, что разные браузеры ведут тут себя
> по-разному. Экспериментируйте. *
>
> Есть, правда, один минус. Говорят, что Яндекс не очень охотно индексирует
> страницы, у которых не выставлен Last-Modified. Так что, если кто-то
> придумает и обоснует альтернативный способ, я буду раз его услышать.
> Динамическое "окно" в закэшированной странице
>
> Ну и напоследок упомяну одну технику, которая может быть полезна в свете
> кэширования. Если вам хочется закэшировать главную (или любую другую)
> страницу сайта, однако мешает один маленький блок, который обязательно
> должен быть динамическим, воспользуйтесь модулем для работы с SSI<http://sysoev.ru/nginx/docs/http/ngx_http_ssi_module.html>.
>
>
> В ту часть страницы, которая должна быть динамической, вставьте вот такой
> "HTML-комментарий":
>
> <!--# include virtual="/get_user_info/" -->
>
> С точки зрения кэша nginx данный комментарий -- обычный текст. Он будет
> сохранен в кэш-файле именно в виде комментария. Однако позже, при прочтения
> кэша, сработает модуль SSI nginx, который обратится к динамическому URL.
> Конечно, по адресу /get_user_info/ должен быть PHP-обработчик, который
> выдает содержимое данного блока. Более подробно данный способ описан в этой
> статье <http://habrahabr.ru/blogs/nginx/65809/>.
>
> Ну и, естественно, не забудьте включить SSI для этой страницы или даже для
> всего сервера:
>
> ssi on;
>
>  [image: Чайник]
>
> * Директива SSI include имеет еще одно, крайне важное свойство. Когда на
> странице встречаются несколько таких директив, то все они начинают
> обрабатываться одновременно, в параллельном режиме. Так что, если у вас на
> странице 4 блока, каждый из которых загружается 200мс, в сумме страница
> будет получена пользователем через 200мс, а не через 800. *
>
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://nginx.org/pipermail/nginx-ru/attachments/20091016/ab164031/attachment.html>


More information about the nginx-ru mailing list