MODX Revolution + nginx + memcached.

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

Общая идея: генерировать страницу средствами MODX-а и сохранять ее код полностью в memcached, чтобы в дальнейшем nginx проверял наличие кода страницы в memcached-е, и если есть, отдавал код сразу, не отправляя запрос на php. Пока что это только для сайтов-визиток и т.п. (так как на таких страницах нельзя использовать логику с учетом авторизации пользователей и т.п.), но в дальнейшем планирую страницы делать полностью статическими, а всю динамику переносить на Javascript + AJAX.

Основные проблемы, с которыми столкнулся:

  1. Больше всего времени потратил на настройку правил для nginx-а (свой вариант покажу ниже).
  2. Битая кодировка кеша больших страниц. Как выяснилось, в php для memcached стояла настройка по умолчанию memcached.compression_threshold=2000. То есть если кешируемая строка больше 1999 символов, то php компрессирует ее. Если получить кеш средствами php, то проблем нет, он и декомпрессирует. А вот nginx за этим не следит и отдает как есть. Полечил топорно: в настройках php указал memcached.compression_threshold=9999999

Итак, код плагина:

<?php
switch($modx->event->name){
case 'OnWebPageComplete': 
if (!empty($modx->resource) 
&& $modx->resource->get('cacheable') 
&& $modx->resource->get('published') 
&& $modx->resource->_output != ''
)

{
$key = '/index.php?';

// Заменяем спецсимвол амперсанта
$key .= str_replace('&', '&',($_SERVER['QUERY_STRING']));

// $modx->log(1, "Key: '{$key}'");

$modx->cacheManager->set($key , $modx->resource->_output);
}
break;;

default:;
}

Да, плагин вешаем на событие OnWebPageComplete. Здесь мы этот момент обсуждали.

Настройка для nginx:

server {
listen 80;
# Указываем свой хост
server_name myhost; 
allow all; 

error_log /var/log/nginx/error_log;

# Указываем путь до директории сайта
root /var/www/site/public_html; 

# Всякие запрещенные папки
location ~ /\.ht {
deny all;
}
location /core/ {
deny all;
}
location /temp/ {
deny all;
}

# Статика	
location ~* \.(jpg|jpeg|gif|png|ico|css|zip|tgz|gz|rar|bz2|doc|xls|exe|pdf|ppt|tar|wav|bmp|rtf|js)$ { 
access_log off;
expires 30d;
}	

# Основное правило обработки запросов	
location / {
# файлы по умолчанию
index index.php index.html;

# попытка получить запрашиваемый файл в именнованной локации
# Попытка - подразумевает возможные неудачи. Перечисление по порядку 
# в указанных вариантах. 
# При запросе к корню сайта будет получен индексный файл index.php,
# так что сработает правило location ~ \.php$
# А правило @modx сработает только если файл не найден
try_files $uri $uri/ @modx;
}

# Именнованная лакация (правило)
location @modx { 
# выполняем подмену на index.php
rewrite ^/(.*)$ /index.php?q=$1 last;
} 

# Правила обработки php
location ~ \.php$
{
# Контент по умолчанию, чтобы нгинкс не отдавал браузеру
# как обычный файл для скачивания
default_type text/html;

# устанавливаем ключ-переменную для запроса к мемкешу
set $memcached_key "default/$uri?$args";

# выполняем запрос к мемкешу
memcached_pass localhost:11211;

# устанавливаем обработчик ошибок
# если будет получена одна из этих ошибок, то ошибка не будет 
# возвращена сразу, а будет отправлен запрос дальше на php
error_page 404 401 405 502 504 = @php;
} 

location @php { 
# Отправляем запрос на Apache или типа того (у кого что)
proxy_pass http://127.0.0.1:8888; # свой локальный порт
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_connect_timeout 120;
proxy_send_timeout 120;
proxy_read_timeout 180;
} 
}

Напоследок хочу отметить, что в запросах учитываются все GET-параметры, так что этот метод годится и для постраничности и т.п. Любое изменение параметров запроса — это новая страница, новый кеш.

UPD: Забыл сказать, что в MODX-е еще надо сменить кеш-провайдер на memcached. По умолчанию в настройках MODX-а указано cache_handler=xPDOFileCache, то есть используется файловый кеш-провайдер. Надо поменять на cache.xPDOMemCached.

И еще момент: чтобы сразу есть настройки, которые необходимо и в сам конфиг-файл переносить. Дело в том, что для чтения системных настроек из базы данных требуется инициализация самого MODX-а. Это звучит очень логично, но не все на этом внимание заостряют. Так вот, инициализация требует предварительного чтения конфигов. И вот что получается — пока MODX не получил данные из базы данных, он использует конфиги файловые, в том числе и значения по умолчанию. А так как настройка cache_handler=cache.xPDOMemCached хранится в базе данных, то в момент инициализации MODX еще ничего о ней не знает, и соответственно использует стандартный файловый кеш-провайдер. И получив настройки из БД, он только потом очухивается, и начинает использовать Memcached. Но до этого успевает записать конфиги в файлы, и вообще каждый раз при старте читает конфиги из файлов.

Вот чтобы этого не происходило и чтобы он сразу использовал Memcached, зайди в core/config/config.inc.php и в $config_options пропиши эту настройку тоже:

$config_options = array (
"cache_handler" => "cache.xPDOMemCached",
);

Вот тогда он сразу будет мемкешед юзать.

Comments

Молодец! Все толково расписал. Насчет только для визиток, я бы не был так категоричен. Nginx много чего умеет…

Так я сказал «пока». Докрутить многое можно. Да и на уровне плагина можно разрулить что кешировать так, а что нет (допустим, личный кабинет не кешировать). Но это так, эксперимент. Я более плотно планирую делать это на уровне node.js То есть нода не все еще умеет, чтобы так сразу фрейм заменить, но ее можно использовать как умный кешер. То есть и данные в памяти держать, и логику рулить можно. В общем, планирую nginx+MODX+node.js

memcache зараза страницу компрессирует как ни крути, а вот с memcached все нормально) вот это у тебя

$key = '/index.php?';

вообще лишнее, можно выкинуть… и добавить еще $expire

$modx->cacheManager->set($key , $modx->resource->_output, $expire);

выставить время жизни кеша. и в nginx правильнее наверно будет

"default/$args"

memcache зараза страницу компрессирует как ни крути

А ты смотри phpinfo() раздел memcache. У него тоже свои настройки есть, типа max_chunk_size или типа того. Наверняка тоже можно поднять значение.

вообще лишнее, можно выкинуть…

и в nginx правильнее наверно будет «default/$args»

Да? А кто запрещает создать еще какой-нибудь внутренний php-файл? И что тогда? И тогда нафиг все побьется. А ты предлагаешь все жестко завязать на единственный php-файл. Не, я так не буду делать.

и добавить еще $expire

Это я может позже поковыряю, но у меня есть подозрение, что это не так работает, как хотелось бы. Думаю, что это значение для самого MODX-а, а не для мемкеша. То есть как работает этот параметр для MODX-а? — кеш есть, но время уже не актуальное, и MODX его игнорит. Но если это так, то gnixn-у на это пофигу будет, он получит кеш и все. Хотя это только предположение, проверять надо.

да настройки есть конечно, аналогичные кстати… но не получилось, возможно механизм сжатия по другому работает. насчет побьется я не совсем уверен, так как неделю назад вообще про nginx не слышал. Но я думаю ты ошибаешся… Пройдись по конфигу nginx и посмотри логику. $expire работает — уже проверял.

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

Я вчера часов 6 с этим конфигом провозился, так что испробовал различные варианты. Смотри вот правило:

Именнованная лакация (правило)     location @modx {         # выполняем подмену на index.php         rewrite ^/(.*)/index.php?q= /index.php?q=1 last;     }  


Так как ЧПУ MODX-а построено полностью на реврайтах, а не на реальных файлах, именно благодаря ему все УРЛы и преобразуются в index.php Но это происходит только потому что запрашиваемый файл не найден. Но если файл будет найден, то этого преобразования не будет, а останется реальный УРЛ. И в итоге ключ /index.php не будет соответствовать никогда в этом случае.

Но это конечно же ситуация исключительная, поэтому конечно ты можешь пренебрегать этим моментом и сделать так, как ты предложил — в 99% случаев оно действительно будет работоспособным. Но у меня свои тараканы в голове и я терпеть не могу неуниверсальность, поэтому у себя оставлю так.

ну в данном случае у тебя

# устанавливаем ключ-переменную для запроса к мемкешу
set  $memcached_key       "default/$uri?$args";

и все! это же касается только memcached. При чем тут реальные файлы? только лишняя переменная=index.php? в ключе

При чем тут реальные файлы? При том, что здесь используется переменная $uri. Как я писал выше, если файл не найден, то УРЛ меняется на /index.php?q=$1 Повторюсь:

# Именнованная лакация (правило)
    location @modx { 
        # выполняем подмену на index.php
        rewrite ^/(.*)$ /index.php?q=$1 last;
    }

Так как в большинстве случаев при ЧПУ файлов тупо нет, то постоянно и происходит подмена УРЛа на index.php. При этом и переменная $uri и принимает это значение. А если файл будет найден, то $uri не будет изменена, и будет иметь значение реально запрошенного адреса. Теперь понятно? Плагин конечно я тоже упростил. Изначально он был примерно такой:

$query = '/';
            
            $query_str = str_replace('&amp;', '&',($_SERVER['QUERY_STRING']));  
            
            if(!empty($query_vars['q'])){
                $query .= $query_vars['q'];
                unset($query_vars['q']);
            }
            else{
                $query .= 'index.php';
            }
            $query .= "?";
            
            $vars = array();
            foreach($query_vars as $k => $v){
                $vars[] = "{$k}={$v}";
            }
            if($vars){
                $query .= implode("&", $vars);
            }
            $modx->log(1,  print_r($query, 1));
            $modx->cacheManager->set($query , $modx->resource->_output);

И то, что сейчас в плагине осталось — это публичный упрощенный вариант, для которого твои замечания очень обоснованный. Но для меня мой nginx-конфиг более актуальный :-)

ну так бы сразу и сказал, что у тебя свои замуты)

как думаешь, имеет смысл memcached на сокеты перекинуть? По идее еще прирост в скорости должен быть… вот эти настройки в обработчике меняю

('memcached_server', $options, 'localhost:11211')

и по идее будет работать? или тут все сложнее устроено?

Честно скажу: сам с сокетами особо не работал. Знаю, что сокеты быстрее всяко, но настройку этого всего не делал.

подскажи как и где переключиться на сокет? пробовал в конфиге прописать как

$config_options = array (
    "cache_handler" => "cache.xPDOMemCached",
    'memcached_server' => 'unix:///var/run/memcached/mem.socket',
    'system_settings_memcached_server' => 'unix:///var/run/memcached/mem.socket',
    'db_memcached_server' => 'unix:///var/run/memcached/mem.socket',

);

не срабатывает нифига! хелп!)))

Я же говорил, что с сокетами особо не заморачивался. Ты топик напиши как сокеты подключал, с какими подводными камнями столкнулся.

я пока что локально подключился и тесты провел, чтоб узнать будет ли какая выгода от такого перехода. Ты как спец по кешированию подскажи — как перевести всю систему кеширования modx на сокеты теперь? я немного не допонимаю, это именно в файле обработчика кеша нужно сделать? или еще где то нужно пилить? по идее нужна только замена

$memcache->connect('localhost', 11211)

на

$memcache->connect('unix:///var/run/memcached/mem.socket', 0)

и все должно пахать!

Так ты в исходники почаще заглядывай. Вот __construct мемкеша:

$servers = explode(',', $this->getOption($this->key . '_memcached_server', $options, $this->getOption('memcached_server', $options, 'localhost:11211')));
foreach ($servers as $server) {
    $server = explode(':', $server);
    $this->memcache->addServer($server[0], (integer) $server[1]);
}
$compressThreshold = $this->getOption($this->key . '_memcached_compress_threshold', $options, $this->getOption('memcached_compress_threshold', array(), '20000:0.2'));

То есть укажи в системных настройках memcached_server и должно все заработать. Это именно что касается memcache. И, ксттаи, там же ответ на твои проблемы с компрессией:

$compressThreshold = $this->getOption($this->key . '_memcached_compress_threshold', $options, $this->getOption('memcached_compress_threshold', array(), '20000:0.2'));

То же самое и для memcached:

$servers = explode(',', $this->getOption($this->key . '_memcached_server', $options, $this->getOption('memcached_server', $options, 'localhost:11211')));

Только вот с компрессией у него лучше дела обстоят, так как он значение берет из системы:

$this->memcached->setOption(Memcached::OPT_COMPRESSION, (boolean) $this->getOption($this->key . '_memcached_compression', $options, $this->getOption('memcached_compression', $options, $this->getOption(Memcached::OPT_COMPRESSION, $options, true))));

И, кстати, как я и предполагал, должна быть возможность в принципе отключить для него компрессию.

да компрессия бог с ней… на memcached работает и ладно. Исходники я видел, но если прописать туда

'unix:///var/run/memcached/mem.socket', 0

то не пашет! неужели ты думаешь я сначала не попробовал?)) выдает ошибку

Error caching time of next auto publishing event
Could not cache context settings for mgr.
Could not cache context settings for web.

я в коде не разбираюсь но у меня чувство что explode(':', $server) это разбор строки… и если сокет прописывать то и тут надо как то менять…

Так ты не пиши 'unix:///var/run/memcached/mem.socket', 0 Это же сразу два параметра. Пиши только unix:///var/run/memcached/mem.socket

я в коде не разбираюсь но у меня чувство что explode(':', $server)

Да, это разбивка строки. Но мне сечас некогда с этим экспериментировать, я пока очень занят.

да я уже и так и сяк пробовал… Как время будет посмотри

а как то можно прописать в обработчике, чтоб он мне логи скинул в журнал чему какая переменная равна?

все вроде понял что, к чему… только вот эта строчка непонятна

$servers = explode(',', $this->getOption($this->key . '_memcached_server', $options, $this->getOption('memcached_server', $options, 'unix:///var/run/memcached/mem.socket')));

explode разбирает на строки — это ясно. getOption — получает параметр по ключу, но их тут сразу несколько одна в одной… вот это мне не совсем ясно… $options мы его в данном случае получаем, или наоборот формируем? мне кажется все дело в этой строке, так как я пробовал все это убирать и просто делал

$this->memcached->addServer('unix:///var/run/memcached/mem.socket', 0);

но не работает! может эти настройки еще где-то в modx торчат?

options—этото,чтоможнопередатьвинициализациюобъекта.Вданномслучаевообщенаэтозабей.ДваразаgetOption(),потомучтовторойраз,этозначениепоумолчанию.Разбейнадвараза,чтобыпонятнейбыло.‘options — это то, что можно передать в инициализацию объекта. В данном случае вообще на это забей. Два раза getOption(), потому что второй раз, это значение по умолчанию. Разбей на два раза, чтобы понятней было. `servers = explode(',', this−>getOption(this->getOption(this->key. '_memcached_server', options,options, this->getOption('memcached_server', $options, 'unix:///var/run/memcached/mem.socket')));` Это буквально так:

$servers_default = $this->getOption('memcached_server', $options, 'unix:///var/run/memcached/mem.socket');

$servers_str = $this->getOption($this->key . '_memcached_server', $options, $servers_default);

$servers = (array)explode(',', $servers_str);

То есть может быть указано несколько серверов. Укажи memcached_server в настройках и все.

ну я логически так и додумал, но ошибка все таже —

Error caching time of next auto publishing event
Could not cache context settings for mgr.
Could not cache context settings for web.

вот у меня чувство что где то еще — localhost:11211 торчит по умолчанию…

Вот и ковыряй. Думаешь у меня всегда все сразу получается? Нет, иногда часами сидишь воюешь, а иногда и днями.

тогда может на $options всетаки нельзя забивать? он ведь у нас в каждой функции идет

$options= array())

либо еще какая то фигня мешает работе с сокетами либо я хз

у меня к тебе одна просьба будет — можешь в своем обработчике закомментировать строчку вот эту

$this->memcached->setOption(Memcached::OPT_COMPRESSION, (boolean) $this->getOption($this->key . '_memcached_compression', $options, $this->getOption('memcached_compression', $options, $this->getOption(Memcached::OPT_COMPRESSION, $options, true))));

сбрось кеш и посмотри какую ошибку выдаст. спасибо!

Мне реально сейчас не до этого. Так что будь настоящим джедаем и воюй до последнего сам.

все разобрался, все правильно я прописывал, надо было просто чуток подождать. настройки тоже кешировались и мои изменения с кешером не сразу применялись. сейчас все по новой прописал, подождал 5 минут — и все ок! Работает!)

и насчет компрессии ты был прав, можно прям в кешере указать, а не лезть в системные настройки-тоже пашет! p.s. и может конечно это кажется только, но админка вроде еще шустрее стала!)

новый косяк обнаружил. если сайтов несколько, а кеш получается обший, то они в кеше перезаписывают настройки друг друга. Это я имею ввиду не кешированные странички, а именно работу в админке. то есть надо какой то общий ключ вводить для сайта по типу site_name… добавить системную настройку cache_prefix и все работает!

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

Админка будет шустрее работать, так как каждый Ajax-запрос — это инициализация MODX-а со всеми конфигами и т.п. А раз они в кеше, то хоть чуть-чуть, но быстрее все.

switch($modx->event->name){
    case 'OnWebPageComplete': 
        if (!empty($modx->resource) 
            && $modx->resource->get('cacheable') 
            && $modx->resource->get('published') 
            && $modx->resource->_output != ''
            && $_SERVER['REQUEST_METHOD'] == 'GET'
        )
            
        {
            $key = 'index.php?';
            
            $key .= str_replace('&', '&',($_SERVER['QUERY_STRING']));
               
            $modx->log(1, "Key:  '{$key}'");
              
            $modx->cacheManager->set($key , $modx->resource->_output);
        }
        break;
        
    default:;
}

Проверка на GET чтобы если отправили форму не кешировалась страница с ответом.

$key = 'index.php?';

Чтобы ключ был красивее. Modx его все равно модифицирует. в

$this->key . '/' . $key;

$this->key это системная настройка cache_key Так что если cache_key = site.com Тогда в конфиге nginx пишем

set  $memcached_key       "site.com$uri?$args";

Проверка на GET чтобы если отправили форму не кешировалась страница с ответом.

Но это как раз и не стоит делать. Ведь GET — это в том числе и постраничность и прочие моменты, включая гет-поиск. Все, что не должно кешироваться — надо выносить в Ajax.

Либо предусматривать какой-то GET-параметр типа nocache=1, чтобы явно указывать, что ее кешировать не надо. Если добавится в запрос этот параметр, для нгинкса это уже будет новый адрес, и он отправит запрос на MODX. MODX не сформирует полный кеш страницы в мемкеш, а значит и нгинкс все время будет отдавать не кешированную страницу.

Приветь!

Николай настроил по заметкам! html контент работает но в админке вот таки логи ошибок

[2016-03-12 15:23:52] (ERROR @ /index.php) Key:  '/index.php?q=news/1453/'
[2016-03-12 15:23:54] (ERROR @ /index.php) Key:  '/index.php?'
[2016-03-12 15:25:26] (ERROR @ /index.php) Key:  '/index.php?'