Full Cache. Простое Frontend кеширование без Nginx

Цель. Снизить нагрузку при большом потоке незарегистрированных пользователей.
Причины нагрузки.
1) Даже при включении бэкенд кеша любого вида(memcache, file, xcache) движок все же ест память, так как структуру данных он выстраивает в любом случае. При большом количестве плагинов растет количество классов и экземпляров этих классов, которые загружаются в память. В данном случае использование бэкенд кеша увеличивает скорость работы за счет кеширования результатов запросов в базу данных. Но размер потребляемой памяти остается тот же. Например, у меня страница топика ела 19 Мб, а список — 30мБ. При этом частично помогает, например, использование ссылок на объекты с помощью & при переборе, а не их копий. Но все же это крошки.
2) Даже при полном кеше БД все же выполняются несколько запросов, таких как получение данных о сессии или update таблицы просмотров. Под нагрузкой и при использовании InnoDb базы «update запрос» может очень сильно тормозить.
Ресурсы и методы
Собственно смысл прост. Он состоит в том, чтобы один раз генерировать страницу, а потом отдавать ее сразу.
Проверять на существование и отдавать кеш надо будет перед запуском движка, что разгрузит потребляемую память.

Как выдавать кеш? Кеш нам надо выдавать как можно раньше, не допуская загрузки всего движка, а затем завершать скрипт принудительно. Хранить будем, например, в папке _cache в корне. Для этого ее предварительно создадим с нужными правами.
Для этого в index.php
После

// Получаем объект конфигурации
require_once("./config/loader.php");

вставляем

/**
 * Проверяем, использовать кеш или нет.
 * Существование $_COOKIE['key'] будет идентифицировать то, что юзер, возможно, авторизован.
 * Кука _key нужна для того, чтобы можно было при авторизации, разлогивании или регистрации исключить кеширование.
 * Далее условия можете ставить какие угодно. 
 * Так как в LS парадигма REST еще не включена, то, 
 * если массивы $_POST и $_GET не пусты, тоже не будем использовать кеширование
 */
$bUseCache = 
    (!isset($_COOKIE['key']) || empty($_COOKIE['key']))	&& 
    (!isset($_COOKIE['_key']) || empty($_COOKIE['_key'])) && 
    empty($_POST) && empty($_GET);

// уникальный путь к кеш файлу, используя переменные запроса и шаблона
$sCacheFile=Config::Get('path.root.server')."/_cache/".md5(Config::Get('view.skin').$_SERVER['REQUEST_URI']).".tmp";
// установим в конфиг

Config::Set('usefullcache',$bUseCache);
Config::Set('fullcache_file',$sCacheFile);

// собственно проверяем, есть ли кеш файл, затем выводим его содержимое и завершаем скрипт
if ($bUseCache && @file_exists($sCacheFile)) {
   echo file_get_contents($sCacheFile);
   exit();
}


Как нам загнать полную страницу в кеш?
Для начала вспомним, как она генерируется. В начале у нас инициализируется Router, который в свою очередь инициализирует Engine, запускает контролеры (экшены), потом передает переменные во Вьюху. После чего мы уже видим готовую страницу.
Это все делается в методе $oRouter->Exec().
В конце метода запускается:
$this->Viewer_Display($this->oAction->GetTemplate());
(в >=5 версиях данный код вынесен в метод Shutdown())
Данный метод проверяет, аяксовый запрос или обычный. В зависимости от результата выводит данные с разными заголовками и в разном формате.

Тут есть два пути: лезть глубже во Viewer или использовать вызов метода прямо в роутере. Плюс первого в том, что мы можем избежать эксепшенов, которые кидает вьювер и смарти, и не кешить их. Но я остановлюсь на втором.

В engine\classes\Router.class.php код

$this->Viewer_Display($this->oAction->GetTemplate());

меняем на

// C помощью буфера получаем страницу в переменную
ob_start();
$this->Viewer_Display($this->oAction->GetTemplate());		
$sOutput = ob_get_contents();
ob_end_clean();

// Затем, используя переменные, которые ранее мы загнали в конфиг, записываем в файл содержимое
if (Config::Get('usefullcache') && $sCacheFile = Config::Get('fullcache_file')) {
    @touch($sCacheFile);
    file_put_contents($sCacheFile,$sOutput);
}
// выводим на экран страницу
echo $sOutput;


Мы забыли только вставить генерацию куки _key:
В classes\modules\user\User.class.php
вместо

/**
 * Ставим куку
 */
if ($bRemember) {
    setcookie('key',$sKey,time()+60*60*24*3,Config::Get('sys.cookie.path'),Config::Get('sys.cookie.host'));
} 

ставим

/**
 * Ставим куку
 */
if ($bRemember) {
    setcookie('key',$sKey,time()+60*60*24*3,Config::Get('sys.cookie.path'),Config::Get('sys.cookie.host'));
} else {
    // to identify user before session starts
    setcookie('_key',$sKey,time()+60*60*24*3,Config::Get('sys.cookie.path'),Config::Get('sys.cookie.host'));
}

А после

/**
 * Дропаем куку
 */
setcookie('key','',1,Config::Get('sys.cookie.path'),Config::Get('sys.cookie.host'));

вставляем

setcookie('_key','',1,Config::Get('sys.cookie.path'),Config::Get('sys.cookie.host'));


Конечно же, кеш нужно чистить
Чищу я одной командой, которую поставил в крон. Например, раз в 5 мин:

# Cache remover
*/5 *  * * *   root    find "ABSOLUTE_PATH_TO_LS/_cache" -name "*.*" -type f -cmin +5 -delete >/dev/null



UPD Совсем забыл. Чтобы кеш работал для аякс запросов, (блоков, тех что в сайдбаре или других, что дерут инфу с сервера) надо их переделать из POST в GET и убрать проверку security_ls_key (для незарегиных это не нужно). Тогда код на использование кеша станет:


$bUseCache = 
    (!isset($_COOKIE['key']) || empty($_COOKIE['key']))	&& 
    (!isset($_COOKIE['_key']) || empty($_COOKIE['_key'])) && 
    empty($_POST) && !isset($_GET['security_ls_key']);


$_GET['security_ls_key'] я оставил, чтоб работало разлогивание.

Для тех, кто хочет Full Highload проект.

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

27 комментариев

avatar
О какой сборке идёт речь в последнем абзаце?
avatar
Я имел в виду сам LS, сорри за неточность. В сборку самого движка.
Можно использовать для кеша и методы DKLAB Backend Cache, но там данные сериализируются и к ним подмешиваются еще и ненужные ключи.
avatar
возможно, я сам попробую сделать pull request? только соображу, как лучше его внедрить
avatar
я за )! готов всячески поддержать!
avatar
так в настройках сервера не оч соображаю, но если будет готовое решение будет реально круто!
avatar
усложняет то, что в LS используются POST запросы для получения данных (например в стриме), а не для их отсылки в формах. Потом результат сложно закешить и вернуть этот кеш при следующем запросе. Строка урла-то не меняется. Надо разбирает постданные, и на их основе строить ключ кеша.
avatar
к сожалению не чего не понял :)

я только верстальщик и дизайнер :) с некоторыми знаниями Smarty и JQ… Как я понимаю есть сложные моменты в реализации…

а вы смотрели 1.0 dev может там как то проще будет?
avatar
Вот схема проекта в идеале. Когда страница генерируется php скриптом (или еще чем-нибудь), она записывается в кеш (в данном случае — memcache, но вообще неважно, хоть в файл). Ключем для кеша (то есть идентификатором, по которому его потом искать) служит URI запроса вместе с GET данными. В следующий раз при идентичном запросе nginx сначала поищет в кеше, используя ключ URI, вернет из кеша или опять запустит скрипт бэкенд php.

схема фронтенд кэширования

У меня вместо nginx кусок кода в index.php, а кеш пишется в файл, имя которого равно ключу кеша. Если POST запрос, то в URI параметр запроса невидны. Поэтому ключ сварганить для них нельзя. Да и не надо, POST запросы используются для отправки данных на сервер, а не для получения.
Но вот в некоторых блоках LS (те же блоки в сайдбаре) данные с сервера как раз-таки получаются POST — ом. Я считаю, что надо получать GET-ом, для их легкого кэширования. Поиск же сделан через GET:
livestreet.ru/search/topics/?q=%D0%BA%D0%B0%D0%BA
И его результаты легко закешить, а потом отдать, потому что URI запроса будет уникальным для разных ?q=.
avatar
Вот если взять этот сайт — livestreet.ru — то какие страницы, кроме Эбаут, тут можно закешировать целиком?
avatar
все можно, просто поставить время его жизни небольшое.
Вот на DKLAB пишут:
При высокой посещаемости даже кэширование на короткий срок (5 минут и меньше) уже дает огромный прирост в производительности, ведь кэш работает очень быстро. Даже закэшировав страницу всего на 30 секунд, вы все равно добьетесь значительной разгрузки сервера, сохранив при этом динамичность обновления данных (во многих случаях обновления раз в 30 секунд вполне достаточно).
Конечно разницы, вы не увидите на небольших нагрузках.
avatar
Проблема в том, что в движке используется концепция MVC, а это значит, что какие-то команды и параметры, использующиеся для определенных действий, а не только для отображения, могут передаваться не только в GET или POST, но и непосредственно в URL. Поэтому такой подход, все же, чреват некорректной работой. Я уж не говорю о таких вещах, как, например, подсчет юзеров в онлайне или счетчик просмотра топиков.

Если уж и кешировать, то на уровне шаблонизатора, тем более, что у Smarty есть встроенные механизмы для этого.
avatar
В URL передаются команды для контроллера — почти в основном это параметры для отображения.
Что до действий, то для них все равно надо передавать какой-то идентификатор или токен, например,
http://livestreet.ru/login/exit/?security_ls_key=240a20f4b501eb1a8a9993bb1bfafab8

По нему можно и отсеивать.
Подсчет юзеров в онлайне считает только активные сессии зарегистрированных пользователей, для которых кеш не будет работать. Для незарегистрированных этот показатель будет обновляться на 2 минуты реже, что некритично.
Счетчик просмотра топиков для больших проектов выносят в отдельный скрипт или блок, что помогает избежать блокировку кеширования всей страницы в целом.
Если уж и кешировать, то на уровне шаблонизатора, тем более, что у Smarty есть встроенные механизмы для этого.
Ну ведь для этого опять же надо инициализировать движок, а смысл кеша был в том, чтобы это избежать. К тому же у нас шаблон генерируется в самом конце. Это было бы актуально, если бы модули вызывались прямо из шаблона.
А фронтенд кеширование прекрасно дружит с MVC фреймворками.
Еще на помощь может прийти Server side include.
avatar
Если уж кэшировать на уровне шаблонизатора, то нужен блиц или что-то подобное. Смарти и высокая производительность — вещи несовместимые.
avatar
да, но если мне отправили письмо или ответили на комментарий, я подожду 10 секунд и уйду, а человек будет ждать думая что я получил письмо, а — нет.

сомнительно
avatar
а это при чем тут? Кеш только для незарегистрированных.
avatar
нельзя ж отправить письмо или комментарий, не будучи авторизованным.
avatar
varnish, господа, юзайте varnish…
avatar
// выводим на экран страницу
echo $sOutput

После этого требуется ";".
avatar
спасибо, исправил
avatar
Кто нибудь испытал на действующем проекте, какие впечатления? Стоит ли это сделать если используется xcache и nginx? Прошу разумного совета.
avatar
Как раз использую эту связку. Если на nginx кэширование не стоит — то стоит. После того, как попробовал кэширование и на энжин-Х, и это кэширование — предпочел второй вариант. Но если нагрузка небольшая — то и кэширование использовать особо смысла нет(по крайней мере, если облако в качестве хостинга).
Кэш пишется на диск, так что скорость отдачи зависит и от него(у меня используются sas-диски). Меня полностью устраивает.
После включения кэширования нагрузка на процессор и диск возврастет(не сильно) — записывать и обновлять кэш кому-то же надо.
avatar
ну писать можно и в память, просто для использования memcache надо переписать стандартный модуль так, чтобы его можно было использовать без инициализации всего движка.
avatar
можно создать в памяти виртуальный диск и обойтись без memcache
avatar
Нет, нет и ещё раз нет!
У меня все полгигабайта лежат в RAMFS, и без xCache (опкод и данные. Да, именно так, memcached — велоипедное излишество) приходится весьма туго, LA при пиковой ~3.5. А так 1 в среднем, ну 1.8~2.3 в пик.
avatar
Стоит ли это сделать если используется xcache и nginx? Прошу разумного совета.
Немного неправильно сформулировал вопрос чуть выше. Nginx как раз и настроен на кеширование и стоит фронтэндом к apache, т.е. более точно если стоит apache+nginx и используется xcache правильно ли будет использовать еще и описанное в топике?
avatar
если есть доступ к конфигам nginx, то лучше сделать, как здесь
avatar
Ошибку понял:
*/5 * * * * root find «ABSOLUTE_PATH_TO_LS/_cache» -name "*.*" -type f -cmin +5 -delete >/dev/null — root здесь лишнее.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.