![]() |
![]() |
|
||
![]() |
|
|||||||||||||||||||||||||||||||||||||||||||||||||
| [15 октября 2009 г.] |
|
В 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 секунд вполне достаточно).
Например, закэшировать главную страницу можно так:
| Листинг 1 |
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 /var/cache/nginx levels= keys_zone=wholepage:50m;
В директиве fastcgi_cache_path я выставляю "пустое" значение для levels. Хотя это немного снижает производительность (файлы будут напрямую создаваться в /var/cache/nginx, без разбиения по директориям), но зато на порядок облегчает отладку и диагностику проблем с кэшем. Поверьте, вам еще не раз придется руками залезать в /var/cache/nginx и смотреть, что там хранится.
fastcgi_cache_valid 200 301 302 304 5m;
В директиве fastcgi_cache_valid мы заставляем кэшировать не только стандартные коды 200 ОК, 301 Moved Permanently и 302 Found, но также и 304 Not Modified. Почему? Давайте вспомним, что означает 304. Он выдается с пустым телом ответа в двух случаях:
fastcgi_cache_key "$request_method|$http_if_modified_since|$http_if_none_match|$host|$request_uri";
Особого внимания заслуживает значение в директиве fastcgi_cache_key. Я привел минимальное рабочее значение этой директивы. Шаг вправо, шаг влево, и вы начнете в ряде случаев получать "неправильные" данные из кэша. Итак:
fastcgi_hide_header "Set-Cookie";
Директива fastcgi_hide_header очень важна. Без нее вы серьезно рискуете безопасностью: пользователи могут получить чужие сессии через сессионную Cookie в кэше. (Правда, в последних версиях nginx что-то было сделано в сторону автоматического учета данного фактора.) Понимаете, как это происходит? На сайт зашел Вася Пупкин, ему выдалась сессия и сессионная Cookie. Пусть кэш на тот момент оказался пустым, и в него записалась Васина Cookie. Затем пришел другой пользователь, получил ответ из кэша, а в нем - и Cookie Васи. А значит, и его сессию тоже.
Можно, конечно, сказать: давайте не будем вызывать session_start() на главной странице, тогда и с Cookies проблем не будет. В теории это так, но на практике данный способ очень неустойчив. Сессии часто стартуют "отложено", и достаточно какой-либо части кода "случайно" вызвать функцию, требующую доступа к сессии, как мы получим дыру в безопасности. А |
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". Здесь существует три решения проблемы:
Думаете, это все аспекты кэширования? |
Статическая главная
Решение
В итоге первые 10 запросов к скрипту-генератору выполнятся "честно" и "нагрузят" сервер. Зато потом они "осядут" в кэше и в течение минуты будут выдаваться уже быстро. Прирост производительности тем больше, чем больше посетителей на сайте.
Вот кусочек конфига nginx, реализующий кэширование с ротацией:
| Листинг 2 |
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 $rand 'sub { return int rand 10 }';
С директивой perl_set все просто. Мы создаем переменную, при использовании которой nginx будет вызывать функцию встроенного в него Perl-интерпретатора. По словам автора nginx, это достаточно быстрая операция, так что мы не будем "экономить на спичках". Переменная принимает случайное значение от 0 до 9 в каждом из HTTP-запросов.
fastcgi_cache_key "$rand|$request_method|...";
Теперь мы замешиваем переменную-рандомизатор в ключ кэша. В итоге получается 10 разных кэшей на один и тот же URL, что нам и требовалось. Благодаря тому, что скрипт, вызываемый при кэш-промахе, выдает элементы главной страницы в случайном порядке, мы получаем 10 разновидностей главной страницы, каждая из которой "живет" 1 минуту (см. fastcgi_cache_valid).
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"? Только тем, что |
Однако есть еще кэш в браузере. И в некоторых случаях браузер может даже не пытаться делать запрос на сервер, чтобы отобразить страницу; вместо этого он достанет ее из собственного кэша. Т.к. у нас ротация, нам такое поведение неудобно: ведь каждый раз, заходя на страницу, пользователь должен видеть новые данные. (На самом деле, если вы все же хотите закэшировать какой-нибудь один вариант, то можно поэкспериментировать с заголовком Cache-Control.)
Директива add_header как раз и передает в браузер заголовок запрета кэширования. Ну а чтобы этот заголовок случайно не размножился, мы вначале убираем из HTTP-ответа то, что записал туда PHP-скрипт (и то, что записалось в nginx-кэш): директива fastcgi_hide_header. Ведь вы, когда пишете конфиг nginx-а, не знаете, что там надумает выводить PHP (а если используется session_start(), то он точно надумает). Вдруг он выставит свой собственный заголовок Cache-Control? Тогда их будет два: PHP-шный и добавленный нами через add_header.
expires -1; # Внимание!!! Эта строка expires необходима! add_header Last-Modified $sent_http_Expires;
Еще один трюк: мы должны выставить Last-Modified равным текущему времени. К сожалению, в nginx нет переменной, хранящей текущее время, однако она магическим образом появляется, если указать директиву expires -1.
Хотя это сейчас (октябрь 2009 г.) не задокументировано, nginx создает переменные вида $sent_http_XXX для каждого заголовка ответа XXX, отданного клиенту. Одной из них мы и пользуемся. |
Почему же так важно выставлять текущим временем этот заголовок? Все довольно просто.
На самом деле, большой вопрос, как поведет себя браузер при наличии одновременно 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/" wait="no" -->
С точки зрения кэша nginx данный
Ну и, естественно, не забудьте включить SSI для этой страницы или даже для всего сервера:
ssi on;
Директива SSI include имеет еще одно, крайне важное свойство. Когда на странице встречаются несколько таких директив, то все они начинают обрабатываться одновременно, в параллельном режиме. Так что, если у вас на странице 4 блока, каждый из которых загружается 200мс, в сумме страница будет получена пользователем примерно через 200мс, а не через 800. На самом деле, время будет чуть больше 200мс, т.к. надо учитывать загруженность остальных потоков nginx (возможно, на других машинах кластера, если запросы распараллеливаются по машинам) и издержки на переключение процессов. |
![]() |
| ||||||||||||||||||||||||
| Дмитрий Котеров | 15 октября 2009 г. ©1999-2010 | | Контакт | Вернуться к оглавлению |