Nginx cache не работает

Подводные камни при использовании кэширования в 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 секунд вполне достаточно).

Читайте также:  Не могу настроить роутер tp link tl wr941nd

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

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

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 нам нужна, т.к. 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 очень важна. Без нее вы серьезно рискуете безопасностью: пользователи могут получить чужие сессии через сессионную 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, реализующий кэширование с ротацией:

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

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

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

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

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

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

Выше мы говорили, что 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: гарантируем перезагрузку страницы

Еще один трюк: мы должны выставить 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-комментарий»:

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

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

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

Источник

Nginx cache: всё новое — хорошо забытое старое

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

Но что делать, когда ваш код “идеален”, все тяжелые запросы вынесены в фон, все, что можно, было закэшировано, а сервер все так же не дотягивает до нужных нам показателей SLA? Если есть возможность, то конечно можно докупить новых машин, распределить часть трафика и забыть о проблеме еще на некоторое время.

Но если вас не покидает чувство, что ваш сервер способен на большее, или есть магический параметр, ускоряющий работу сайта в 100 раз, то можно вспомнить о встроенной возможности nginx, позволяющей кэшировать ответы от бэкенда. Давайте разберем по порядку, что это, и как это может помочь увеличить количество обрабатываемых запросов сервером.

Что такое Nginx cache и как он работает?

Nginx кэш позволяет значительно сократить количество запросов на бэкенд. Это достигается путем сохранения HTTP ответа, на определенное время, а при повторном обращении к ресурсу, отдачи его из кэша без проксирования запроса на бекенд. Кэширование, даже на непродолжительный период, даст значительный прирост к количеству обрабатываемых запросов сервером.

Перед тем как приступить к конфигурации nginx, необходимо убедиться, что он собран с модулем “ngx_http_proxy_module”, так как с помощью этого модуля мы и будем производить настройку.

Для удобства можно вынести конфигурацию в отдельный файл, например “/etc/nginx/conf.d/cache.conf”. Давайте рассмотрим директиву “proxy_cache_path”, которая позволяет настроить параметры хранения кэша.

“/var/lib/nginx/proxy_cache” указывает путь хранения кэша на сервере. Именно в эту директорию nginx будет сохранять те самые файлы с ответом от бэкенда. При этом nginx не будет самостоятельно создавать директорию под кэш, об этом необходимо позаботиться самому.

“levels=1:2” — задает уровень вложенности директорий с кэшем. Уровни вложенности указываются через “:”, в данном случае будет созданы 2 директории, всего допустимо 3 уровня вложенности. Для каждого уровня вложенности доступны значения от 1 до 2, указывающие, как формировать имя директории.

Важным моментом является то, что имя директории выбирается не рандомно, а создается на основе имени файла. Имя файла в свою очередь является результатом функции md5 от ключа кэша, ключ кэша мы рассмотрим чуть позже.

Давайте посмотрим на практике, как строится путь до файла кэша:

“keys_zone=proxy_cache:15m” параметр задает имя зоны в разделяемой памяти, где хранятся все активные ключи и информация по ним. Через “:” указывается размер выделяемой памяти в Мб. Как заявляет nginx, 1 Мб достаточно для хранения 8 тыс. ключей.

“max_size=1G” определяет максимальный размер кэша для всех страниц, при превышении которого nginx сам позаботится об удалении менее востребованных данных.

Также есть возможность управлять временем жизни данных в кэше, для этого достаточно определить параметр “inactive” директивы “proxy_cache_path”, который по умолчанию равен 10 минутам. Если в течение заданного в параметре “inactive” времени к данным кэша не было обращений, то эти данные удаляются, даже если кэш еще не “скис”.

Что же из себя представляет этот кэш? На самом деле это обычный файл на сервере, в содержимое которого записывается:

• ключ кэша;
• заголовки кэша;
• содержимое ответ от бэкенда.

Если с заголовками и ответом от бэкенда все понятно, то к “ключу кэша” есть ряд вопросов. Как он строится и как им можно управлять?

Для описания шаблона построения ключа кэша в nginx существует директива “proxy_cache_key”, в которой в качестве параметра указывается строка. Строка может состоять из любых переменных, доступных в nginx.

Символ “:” между параметром куки и get-параметром используется для предотвращения коллизий между ключами кэша, вы можете выбрать любой другой символ на ваше усмотрение. По умолчанию nginx использует следующую строку для формирования ключа:

Следует отметить следующие директивы, которые помогут более гибко управлять кэшированием:

proxy_cache_valid — Задает время кэширования ответа. Возможно указать конкретный статус ответа, например 200, 302, 404 и т.д., либо указать сразу все, с помощью конструкции “any”. В случае указания только времени кэширования, nginx по дефолту будет кэшировать только 200, 301 и 302 статусы.

В этом примере мы установили время жизни кэша в 15 минут, для статусов 200, 301, 302 (их nginx использует по умолчанию, так как мы не указали конкретный статус). Следующей строчкой установили время кэширования в 15 секунд, только для ответов со статусом 404.

proxy_cache_lock — Эта директива поможет избежать сразу нескольких проходов на бэкенд за набором кэша, достаточно установить значение в положении “on”. Все остальные запросы будут ожидать появления ответа в кэше, либо таймаут блокировки запроса к странице. Соответственно, все таймауты возможно настроить.

proxy_cache_lock_age — Позволяет установить лимит времени ожидания ответа от сервера, после чего на него будет отправлен следующий запрос за набором кэша. По умолчанию равен 5 секундам.

proxy_cache_lock_timeout — Задает время ожидания блокировки, после чего запрос будет передан на бэкенд, но ответ не будет закэширован. По умолчанию равен 5 секундам.

proxy_cache_use_stale — Еще одна полезная директива, позволяющая настроить, при каких случаях возможно использовать устаревший кэш.

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

proxy_cache_bypass — Задает условия, при которых nginx не станет брать ответ из кэша, а сразу перенаправит запрос на бэкенд. Если хотя бы один из параметров не пустой и не равен “0”. Пример:

proxy_no_cache — Задает условие при котором nginx не станет сохранять ответ от бэкенда в кэш. Принцип работы такой же как у директивы “proxy_cache_bypass”.

Возможные проблемы при кэшировании страниц

Как уже упоминалось выше, вместе с кешированием HTTP ответа nginx сохраняет полученные от бэкенда заголовки. Если на вашем сайте используется сессия, то сессионная кука также будет закэширована. Все пользователи, зашедшие на страницу, которую вам посчастливилось закэшировать, получат ваши персональные данные, хранящиеся в сессии.

Следующая задача, с которой придется столкнуться — это управление кэшированием. Конечно можно установить незначительное время кэша в 2-5 минут и этого будет достаточно в большинстве случаев. Но не во всех ситуациях такое применимо, поэтому будем изобретать свой велосипед. Теперь обо всем по порядку.

Управление сохранением cookie

Кэширование на стороне nginx накладывает некоторые ограничения на разработку. Например, мы не можем использовать сессии на закэшированных страницах, так как пользователь не доходит до бэкенда, еще одним ограничением будет отдача cookies бэкендом. Так как nginx кэширует все заголовки, то чтобы избежать сохранения чужой сессии в кэше, нам нужно запретить отдачу cookies для кэшируемых страниц. В этом нам поможет директива “proxy_ignore_headers”. В качестве аргумента перечисляются заголовки, которые должны быть игнорированы от бэкенда.

Этой строкой мы игнорируем установку cookies с проксируемого сервера, то есть пользователь получит ответ без заголовка “Set-Cookies”. Соответственно все, что бэкенд попытался записать в cookie, будет проигнорировано на стороне клиента, так как он даже не узнает, что ему что-то предназначалось. Это ограничение в установке cookie следует учесть при разработке приложения. Например для запроса авторизации можно отключить игнорирование заголовка, чтобы пользователь получил сессионную куку.

Также следует учитывать время жизни сессии, его можно посмотреть в параметре “session.gc_maxlifetime” конфига php.ini. Представим, что пользователь авторизовался на сайте и приступил к просмотру новостной ленты, все данные при этом уже есть в nginx кэше. Через некоторое время пользователь замечает, что его авторизация пропала и ему снова нужно проходить процесс авторизации, хотя все это время он находился на сайте, просматривая новости. Это произошло потому, что на все его запросы nginx отдавал результат из кэша, не передавая запрос на бэкенд. Поэтому бэкенд решил, что пользователь неактивен и спустя время указанное в “session.gc_maxlifetime” удалил файл сессии.

Чтобы этого не происходило, мы можем эмулировать запросы на бэкенд. Например через ajax посылать запрос, который будет гарантированно проходить на бэкенд. Чтобы пройти на бэкенд мимо nginx кэша, достаточно отправить POST запрос, также можно использовать правило из директивы “proxy_cache_bypass”, либо просто отключить кэш для этой страницы. Запрос не обязательно должен что-то отдавать, это может быть файл с единственной строкой, стартующей сессию. Цель такого запроса — продлить время жизни сессии, пока пользователь находится на сайте, и на всего его запросы nginx добросовестно отдает закэшированные данные.

Управление сбросом кэша

Вначале необходимо определиться с требованиями, какую цель мы пытаемся достигнуть. Допустим, на нашем сайте есть раздел с текстовой трансляцией популярных спортивных событий. При загрузке страница отдается из кэша, дальше все новые сообщения приходят по сокетам. Для того, чтобы при первой загрузке пользователь видел актуальные сообщения на текущее время, а не 15 минутной давности, нам необходимо иметь возможность самостоятельно сбрасывать кэш nginx в любой момент времени. При этом nginx может быть расположен не на той же машине, что и приложение. Также одним из требований к сбросу будет возможность удаления кэша, сразу по нескольким страницам за раз.

Перед тем как начать писать свое решение, посмотрим, что предлагает nginx из “коробки”. Для сброса кэша в nginx предусмотрена специальная директива “proxy_cache_purge”, в которой записывается условие сброса кэша. Условие на самом деле является обычной строкой, которая при непустом и не “0” значении удалит кэш по переданному ключу. Рассмотрим небольшой пример.

Пример взят с официального сайта nginx.

За сброс кэша отвечает переменная $purge_method, которая является условием для директивы “proxy_cache_purge” и по дефолту установлена в “0”. Это означает, что nginx работает в “обычном” режиме (сохраняет ответы от бэкенда). Но если изменить метод запроса на “PURGE”, то вместо проксирования запроса на бэкенд с сохранением ответа будет произведено удаление записи в кэше по соответствующему ключу кэширования. Также возможно указать маску удаления, указывая знак “*” на конце ключа кэширования. Тем самым нам не нужно знать расположения кэша на диске и принцип формирования ключа, nginx берет на себя эти обязанности. Но есть и минусы этого подхода.

  • Директива “proxy_cache_purge” доступна как часть коммерческой подписки
  • Возможно только точечное удаление кэша, либо по маске вида <ключ кэша>“*”

Так как адреса кэшируемых страниц могут быть совершенно разными, без общих частей, то подход с маской “*” и директивой “proxy_cache_purge” нам не подходит. Остается вспомнить немного теории и открыть любимую ide.

Мы знаем, что nginx кэш — это обычный файл на сервере. Директорию для хранения файлов кэша мы самостоятельно указали в директиве “proxy_cache_path”, даже логику формирования пути до файла от этой директории мы указали с помощью “levels”. Единственное, чего нам не хватает, это правильного формирования ключа кэширования. Но и его мы можем подсмотреть в директиве “proxy_cache_key”. Теперь все что нам остается сделать это:

  • сформировать полный путь до страницы, в точности как это указано в директиве “proxy_cache_key”;
  • закодировать полученную строку в md5;
  • сформировать вложенные директории пользуясь правилом из параметра “levels”.
  • И вот у нас уже есть полный путь до файла кэша не сервере. Теперь все, что нам остается, это удалить этот самый файл. Из вводной части мы знаем, что nginx может быть расположен не на машине приложения, поэтому необходимо заложить возможность удалять сразу несколько адресов. Снова опишем алгоритм:
  • Сформированные пути к файлам кэша мы будем записывать в файл;
  • Напишем простой сценарий на bash, который поместим на машину с приложением. Его задачей будет подключиться по ssh к серверу, где у нас находится кэширующий nginx и удалить все файлы кэша, указанные в сформированном файле из шага 1;

Перейдем от теории к практике, напишем небольшой пример, иллюстрирующий наш алгоритм работы.

Шаг 1. Формирование файла с путями до кэша.

Обратите внимание, что в переменной “$urls” содержатся url закэшированных страниц, уже в формате “proxy_cache_key”, указанном в конфиге nginx. Url выступает неким тегом для выводимых сущностей на странице. Например, можно создать обычную таблицу в бд, где каждый сущности будет сопоставлена конкретная страница, на которой она выводится. Тогда при изменении каких-либо данных мы можем сделать выборку по таблице и удалить кэш всех необходимых нам страниц.

Шаг 2. Подключение на кэширующий сервер и удаление файлов кэша.

Приведенные примеры несут ознакомительный характер, не стоит использовать их в production. В примерах опущены проверки входных параметров и ограничения команд. Одна из проблем с которой можно столкнутся — это ограничение длины аргумента команды “rm”. При тестировании в dev окружении на небольших объемах это можно легко упустить, а в production получить ошибку “rm: Argument list too long”.

Кэширование персонализированных блоков

Давайте подведем итог, что нам удалось сделать:

  • снизили нагрузку на бэкенд;
  • научились управлять кэшированием;
  • научились сбрасывать кэш в любой момент времени.

Но не все так хорошо, как может показаться на первый взгляд. Сейчас, наверное, если не у каждого первого, то точно у каждого второго сайта есть функционал регистрации/авторизации, после прохождения которых, мы захотим вывести имя пользователя где-нибудь в шапке. Блок с именем, является уникальным и должен отображать имя пользователя, под которым мы авторизованы. Так как nginx сохраняет ответ от бэкенда, а в случае со страницей — это html содержимое страницы, то и блок с персональными данными также будет закэширован. Все посетители сайта будут видеть имя первого пользователя прошедшего на бэкенд за набором кэша.
Следовательно, бэкенд не должен отдавать блоки в которых находится персональная информация, чтобы эта информация не попала под nginx кэш.

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

Что такое SSI и как он работает

SSI (Server-Side Includes, включения на стороне сервера) — это некий набор команд, встраиваемых в html страницу, указывающие серверу, что нужно сделать.

Вот некоторый перечень таких команд (директив):

• if/elif/else/endif — Оператор ветвления;
• echo — Выводит значения переменных;
• include — Позволяет вставлять содержимое другого файла в документ.
Как раз о последней директиве и пойдет речь. Директива include имеет два параметра:
• file — Указывает путь к файлу на сервере. Относительно текущей директории;
• virtual — Указывает виртуальный путь к документу на сервере.

Нас интересует параметр “virtual”, так как указывать полный путь до файла на сервере не всегда удобно, либо в случае распределенной архитектуры файла на сервере попросту нет. Пример директивы:

Для того, чтобы nginx начал обрабатывать ssi вставки, необходимо модифицировать location следующим образом:

Теперь все запросы, обрабатываемые location “/”, будут иметь возможность выполнять ssi вставки.

Как же во всей этой схеме будет проходить наш запрос?

  • клиент запрашивает страницу;
  • Nginx проксирует запрос на бэкенд;
  • бэкенд отдает страницу с ssi вставками;
  • результат сохраняется в кэш;
  • Nginx “дозапрашивает” недостающие блоки;
  • итоговая страница отправляется клиенту.

Как видно из шагов, в nginx кэш попадет ssi конструкции, что позволит не кэшировать персональные блоки, а клиенту будет отправлена уже готовая html страница со всеми вставками. Вот наша подгрузка работает, nginx самостоятельно запрашивает недостающие блоки страницы. Но как и у любого другого решения этот подход имеет свои плюсы и минусы. Представим, что на странице есть несколько блоков, которые должны отображаться по-разному в зависимости от пользователя, тогда каждый такой блок будет заменен на ssi вставку. Nginx, как и ожидалось, запросит каждый такой блок с бэкенда, то есть один запрос от пользователя породит сразу несколько запросов на бэкенд, чего совсем бы не хотелось.

Избавляемся от постоянных запросов к бэкенду через ssi

Для решения этой задачи нам поможет модуль nginx “ngx_http_memcached_module”. Модуль позволяет получать значения от сервера memcached. Записать через модуль не получится, об этом должен позаботиться сервер приложения. Рассмотрим небольшой пример настройки nginx в связке с модулем:

В переменной $memcache_key мы указали ключ, по которому nginx попробует получить данные из memcache. Параметры подключения к серверу memcache задаются в директиве “memcached_pass”. Подключение можно указать несколькими способами:

• IP адрес и порт;

Если nginx удалось получить ответ от сервера кэша, то он отдает его клиенту. В случае когда данных в кэше нет, запрос будет передан на бэкенд через “@fallback”. Эта небольшая настройка memcached модуля под nginx поможет нам сократить количество проходящих запросов на бэкенд от ssi вставок.

Надеемся, эта статья была полезна и нам удалось показать один из способов оптимизации нагрузки на сервер, рассмотреть базовые принципы настройки nginx кэширования и закрыть возникающие проблемы при его использовании.

Источник

Оцените статью