Nginx + Memcached + SSI - кеширование страниц и блоков (partials)

В одной из предыдущих статей мы рассмотрели, каким образом можно реализовать кеширование страниц с помощью Varnish и ESI. В этой статье рассмотрим альтернативное решение - на основе двух суперзнаменитых продуктов - nginx и memcached.
Оба не нуждаются в представлении, а о том, как на основе их можно значительно увеличить эффективность работы Вашего сайта, поговорим ниже.
Зачем нужно кешировать страницы?
Довольно детально принципы кеширования страниц и блоков были рассмотрены в предыдущей статье (Varnish + ESI).
В качестве краткого повторения, несколько основных идей и преимуществ кеширования на уровне странц:
- Кешируя страницы на отдающем сервере, Вы освобождаете ощутимое количество ресурсов на бекендах
- В случае медленных страниц, значительно уменьшается время ожидания страниц Вашего сайта (ускоряется его работа)
- Как показывает практика, во многих случаях внедрять систему кеширования странц в готовый проект легче, чем систему кеширования запросов
Nginx и memcached
Nginx позволяет читать данные из Мemcached - для этого Вам необходим модуль HttpMemcachedModule.
Самый простой пример - проверяем есть ли страница в кеше и, если есть, достаем из кеша, иначе - делаем запрос к бекенду.
server {
location / {
set $memcached_key $uri; # Ключ для проверки в memcached
memcached_pass 127.0.0.1:11211; # Параметры подключения
default_type text/html; # Заголовок по умолчанию
error_page 404 = @fallback; # 404 - данные в кеше не найдены
}
location @fallback {
proxy_pass backend; # Бекенд
}
}
В данный момент Nginx не умеет сохранять значения в memcached - только читать данные. Значит задача по сохранению страниц в кеш ложится на бекенд. Выглядит это приблизительно так (PHP):
<? $memcache = new Memcache(); # какой-то код ob_start(); # визуализация страницы - html $html = ob_get_clean(); $memcache->set($_SERVER['REQUEST_URI'], $html); echo $html; ?>
Кеширование блоков и SSI
Если у Вас не самый примитивный сайт (а он у Вас не такой), логика кеширования страниц будет несколько усложнена различными динамическими элементами страницы (блок авторизации, динамическое меню и т.п.). Подобные задачи решаются c помощью SSI - server side includes. Кроме всего прочего использование SSI экономит память, т.к. для каждой страницы Вы не будете хранить ее исходник целиком, а только внутреннюю (изменяемую) часть.
Синтаксис SSI очень простой и выглядит так:
<!--# include virtual="/authentication.php" -->
Следует отметить, что SSI - это уровень Web сервера. Для прилжения этот механизм полностью прозрачен. В приведенном примере, на месте вызова SSI, Web сервер (nginx) просто сделает еще один запрос к бекенду. Например, если у Вас два SSI вызова на страницу, то клиентский запрос к каждой странице будет генерировать три запроса к бекенду. Конечно, само по себе это бессмысленно, но в связке с кешированием представляет из себя мощный инструмент оптимизации систем.
Практический пример
Теперь соберем все выше изложенное в последовательный рецепт решения рассматриваемой задачи.
Для начала Вам необходимо выделить блоки в Вашем базовом (глобальном) шаблоне и заменить их вызовами SSI. Пример на PHP:
<html> <body> <h1>Тестируем nginx + memcached + ssi</h1> <div class="auth"> <!--# include virtual="/authentication.php" --> </div> <!--# include virtual="/somepage.php" --> </body> </html>
Как видно, на странице есть два блока - блок авторизации и контентный блок (содержимое странички). В нашем примере блок авторизации будет персональным (допустим, после входа, там будет имя пользователя). Блок содержимого не будет персональным (т.е. он будет общим для всех пользователей).
Теперь необходимо настроить nginx для обработки SSI вызовов и использованию memcached:
# Upstream бекенда
upstream backend {
server 127.0.0.1:8081;
}
server {
listen 80;
server_name test.com;
root /var/www/test;
index index.php;
location / {
# Все POST запросы отправляем на бекенд (не кешируя)
if ($request_method = POST) {
proxy_pass http://backend;
break;
}
# Включаем обработку SSI
ssi on;
default_type text/html;
# Проверяем в мемкеше
set $memcached_key "$uri";
memcached_pass localhost:11211;
proxy_intercept_errors on;
error_page 404 502 = @process;
}
# Сюда запрос приходит, если его небыло в кеше
location @process
{
proxy_pass http://backend;
ssi on;
}
}
# Обычный бекенд
server {
listen 8081;
location ~* \.(php)$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/test$fastcgi_script_name;
}
}
Следующим шагом в приложении необходимо будет реализовать сохранение исходников страниц в memcached.
Кеширование персонализированных блоков
Ясно, что для кеширования персонализированных блоков необходимо добавить идентификатор пользователя (не вздумайте использовать внутренний ID пользователя) в ключ memcached. SSI вызов необходимо пометить, чтобы отличать персональные блоки от общих. Т.е. персональный вызов SSI будет иметь такой вид:
<!--# include virtual="/uid/authentication.php" --> </div>
Для генерации memcached ключей с идентификатором пользователя можно использовать куки (устанавливать его будет приложение при авторизации пользователя):
set $memcached_key "$uri:$cookie_uid";
Соответственно, необходимо в nginx добавить обработчик для “/uid” запросов:
location /uid {
ssi on;
default_type text/html;
set $memcached_key "$uri:$cookie_uid";
memcached_pass localhost:11211;
proxy_intercept_errors on;
error_page 404 502 = @process;
}
Естественно, в самом приложении все “/uid” запросы должны кешироваться с идентификатором пользователя:
$m->set($_SERVER['REQUEST_URI'] . ':' . $_COOKIE['uid'], $html);
Следует обратить внимание
Недавно появился продукт, который должен стать типичным решением при кешировании страниц - Twicecache. Пока он на очень ранней стадии и доступны только его исходники. Будем внимательно следить.


error_page 404 @fallback;
должно быть
error_page 404 = @fallback;
чтобы HTTP код был не 404, а такой, какой должен быть в случае промаха мимо кеша
@ash2k
Спасибо, исправил ошибку
Настроил все согласно вашему примеру, -все замечательно работает за одним исключением.
На запросы вида http://site/ nginx выдает 404, если зайти на http://site/index.php все отрабатывает (ответ ложится в кеш) и http://site/ тоже начинает работать. После очистки кеша nginx снова отдает 404 на http://site/
@KOT
Похоже, что пропущен “index index.php;” - добавьте это во все виртуальные хосты, где это актуально
Не хватает
root /var/www/test;
index index.php;
в
# Обычный бекенд
server {
listen 8081;
location ~* \.(php)$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/test$fastcgi_script_name;
}
Кроме того nginx при вызове http://site/ в $uri будет “/index.php” а не “/” как в php $_SERVER['REQUEST_URI']. По этому нужно либо в nginx заменить $uri на $request_uri либо в PHP заменить $_SERVER['REQUEST_URI'] на $_SERVER["DOCUMENT_URI"] - тогда индексы будут кешироватся и обрабатываться правильно.
P.S. 2Admin: Удалите плиз мои комментарии от 07:15 и 07:17 (напостил лишних так как отправленные не отображались).
Денис, скажите пожалуйста, обязательно ли ставить Nginx на отдельную машину или же его использование будет эффективным даже на одной машине с Апачем? Спрашиваю потому, что сейчас у нашего проекта средств хватит только на один сервер, но мы сразу хотим поставить предложенную вами связку фронтенд+бекенд+кеш
@Начинающий
Вовсе не обязательно. Nginx гораздо лучше подходит (намного эффективнее) для отдачи статики (картинок, файлов стилей и т.п.), поэтому даже на одном сервере лучше использовать связку Nginx + Apache чем просто Apache. В Вашем случае Nginx будет принимать все запросы на себя - статику будет отдавать сам, а запросы к php будет перенаправлять на Apache (который, скажем, будет слушать 81 порт). Обратите внимание на настройки количества процессов в Apache - важно, чтобы они умещались в памяти и не вылазили в своп.
Спасибо за ответ, Денис. Продолжая изучать эту тему я наткнулся на еще один способ ускорения - серверную утилиту eAccelerator, которая ускоряет запуск php-скриптов. Скажите, а не конфликтует ли она с Nginx и Memcached?
@Начинающий
Утилита отличная, советую использовать! На nginx и memcached никак не влияет и не конфликтует