Кеширование часть 1: кешируем алиасы путей

Главные вкладки

Аватар пользователя seaji seaji 14 сентября 2008 в 23:32

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

не работает нормальный режим кеширования для анонимов.

Вот ссылки по теме:
http://drupal.ru/node/18873
http://drupal.org/node/231190
http://drupal.ru/node/18162

Вот я и решил писать свою схему кеширования.
Начну с самого легкого, с кеширования синонимов ссылок (по мотивам вот этого топика: http://drupal.ru/node/19163)

Логика кеширования:

  1. При инициализации загружаем набор алиасов из специальной таблицы кеша (ее надо сделать). Ключ это данная, конкретная url - ка.
  2. Функцию drupal_lookup_path() изменяем таким образом, что она ищет сначала алиас в загруженном наборе алиасов и если не находит, то делает запрос к базе, одновременно устанавливаем флаг "обновить кеш алиасов".
  3. При выходе смотрим есть ли флаг "обновить кеш алиасов" и если есть, то складываем наш массив алиасов для данной страницы в базу данных. Одновременно увеличиваем счетчик обновления кеша на 1.
  4. При запуске крона удаляем те записи кеша, количество обновлений которого, скажем, больше 5-10 (на ваш выбор).

Технические детали:

Изменяем файл path.inc таким образом:
Дописываем в конец функции drupal_init_path() следующий код:
<?php
$result = db_query("SELECT data FROM url_alias_cache WHERE cid = '%s' AND page = %d", $_GET['q'], $_GET['page']);
$alias_result = db_fetch_array($result);
if (!is_null($alias_result['data'])) {
$GLOBALS['alias_cache'] = unserialize($alias_result['data']);
}
?>
функцию drupal_lookup_path() изменяем совсем чуть чуть:
делаем раз:
<?php $map = (array)$GLOBALS['alias_cache']; ?>
делаем два:
<?php
$GLOBALS['alias_cache'][$path] = $alias;
$GLOBALS['alias_cache']['save'] = TRUE;
?>
это в случае если алиас в нашем массиве не найден и нужно сделать запрос в базу.
делаем три:
<?php
$GLOBALS['alias_cache'][$src] = $path;
$GLOBALS['alias_cache']['save'] = TRUE;
?>
это дублирует предыдущий случай, только для случая если мы ищем системный путь по заданному алиасу.
Дописываем функцию path_exit(). Она сохранит новый кеш алиасов, если были изменения:
<?php
// path_alias_cache_insert
function path_exit() {
$alias_cache = $GLOBALS['alias_cache'];
if ($alias_cache['save']) {
unset($alias_cache['save']);
$alias_cache['count'] = $alias_cache['count'] + 1;
$data = serialize($alias_cache);
db_query("DELETE FROM {url_alias_cache} WHERE cid = '%s' AND page = %d", $_GET['q'], (int)$_GET['page']);
db_query("INSERT INTO {url_alias_cache} (cid, page, count, data) VALUES ('%s', %d, %d, '%s')", $_GET['q'], (int)$_GET['page'], $alias_cache['count'], $data);
db_query ('OPTIMIZE TABLE {url_alias_cache}');
}
}
?>
Для чистки кеша дописываем функцию path_cron()
<?php
function path_cron() {
db_query("DELETE FROM {url_alias_cache} WHERE count > %d", 5);
db_query ('OPTIMIZE TABLE {url_alias_cache}');
}
?>

Новая таблица в базе данных

Как вы уже поняли необходима новая таблица в базе данных url_alias_cache
Вот ее структура:

CREATE TABLE `url_alias_cache` (
  `cid` varchar(255) NOT NULL,
  `page` int(11) NOT NULL,
  `count` int(3) NOT NULL default '0',
  `data` longtext NOT NULL,
  PRIMARY KEY  (`cid`,`page`),
  KEY `count` (`count`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Те, кого испугала необходимость ковыряться в базе могут не бояться. Я прикладываю к этому посту архив с модулем, который инсталирует все необходимые таблицы при включении, а так же позволит их удалить. В этом архиве, так же, приложен измененный файл path.inc (для Drupal 5.10), который вы можете просто скопировать в папку includes

Предупреждение

Если вы скопируете файл path.inc из приложенного архива в папку includes, то может произойти ошибка базы данных т.к. необходима таблица. Поэтому сначала включите модуль.

Для гуру привожу измененный код drupal_lookup_path(). Изменения подчеркнуты.
<?php
function drupal_lookup_path($action, $path = '') {
// $map keys are Drupal paths and the values are the corresponding aliases
static $map = array(), $no_src = array();
static $count;
$map = (array)$GLOBALS['alias_cache'];
--------------------------------------
// Use $count to avoid looking up paths in subsequent calls if there simply are no aliases
if (!isset($count)) {
$count = db_result(db_query('SELECT COUNT(pid) FROM {url_alias}'));
}

if ($action == 'wipe') {
$map = array();
$no_src = array();
}
elseif ($count > 0 && $path != '') {
if ($action == 'alias') {
if (isset($map[$path])) {
return $map[$path];
}
$alias = db_result(db_query("SELECT dst FROM {url_alias} WHERE src = '%s'", $path));
$map[$path] = $alias;
$GLOBALS['alias_cache'][$path] = $alias;
-----------------------------------------
$GLOBALS['alias_cache']['save'] = TRUE;
-----------------------------------------
return $alias;
}
// Check $no_src for this $path in case we've already determined that there
// isn't a path that has this alias
elseif ($action == 'source' && !isset($no_src[$path])) {
// Look for the value $path within the cached $map
if (!$src = array_search($path, $map)) {
if ($src = db_result(db_query("SELECT src FROM {url_alias} WHERE dst = '%s'", $path))) {
$map[$src] = $path;
$GLOBALS['alias_cache'][$src] = $path;
---------------------------------------
$GLOBALS['alias_cache']['save'] = TRUE;
---------------------------------------
}
else {
// We can't record anything into $map because we do not have a valid
// index and there is no need because we have not learned anything
// about any Drupal path. Thus cache to $no_src.
$no_src[$path] = TRUE;
}
}
return $src;
}
}
return FALSE;
}
?>

ВложениеРазмер
Иконка пакета url_alias_cache.zip3.86 КБ

Комментарии

Аватар пользователя seaji seaji 14 сентября 2008 в 23:53

По тестам на локальной машине.
Без кеша: 98 запроса к базе (идет установка кеша алиасов)
С кешем : 51 запроса к базе
Имеем минус 47 запросов к базе. Это на главной странице форума.

Аватар пользователя seaji seaji 15 сентября 2008 в 0:11

Да, точно. Я делал для пятерки.
Для Drupal 6 видимо нужно "обработать напильником"
Нечно подобное предлагается здесь: http://drupal.org/node/100301 для шестерки, и даже куча патчей приложено. Разбираться в это времени не было. Было легче написать самому.

Аватар пользователя seaji seaji 15 сентября 2008 в 0:17

Некоторые, кстати, предлагают ввести новое поле в таблицу node.
Предлагают туда записывать либо прямо алиасы - в таком случае мы теряем возможность создавать несколько алиасов на одну страницу, либо ключи на таблицу алиасов, чтоб все обрабатывать одним джоинтом (по моему минусы те же).

Аватар пользователя pluser pluser 15 сентября 2008 в 0:38

Вообще не в тему выскажусь но я заметил два типа ускорения работы любого сайта.

  • Первый это визуальный.
  • Второй фактический.

Как я выяснил для себя визуальный работает более стабильно чем фактический сделанный путём ковыряния ядра.
Почему?
1) Ковыряя ядро я каждый раз создавал себе проблему при обновлении до новой версии так как был вынужден сравнивать версии.
2) Пару раз я так хорошо "отоптимизировал" что убил нафиг системную таблицу и мучался потом часов пять восстанавливая.
3) В итоге после файлового кеширования я получал некоторый лаг до момента начала загрузки которого небыло до файл-кэширования стало шустрее но лаг всё портил вроде быстро грузится но в начале торможение.

Что я называю визуальным ускорением? Оптимизацию структуры сайта таким образом чтобы последовательность была следующей:
Сначала контент потом стили и в конце скрипты.
Для меня актуально кол-во картинок на сайте так как каждая из них делала небольшой лаг в виду задержки обращения к серверу из за расстояния.
Чем больше элементов пусть даже маловесящих тем больше время, но задержка всегда примерно равна и она обычно больше чем грузится мини гифчик.
И ещё у меня очевидно большой лаг из за переадресации на https Smile
Но это я решил предложив посетителям два способа входа один защищённый другой нет.
Просто разные домены сделал.

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

Аватар пользователя seaji seaji 15 сентября 2008 в 0:59

Кеширование на файлах я так же отношу к тупиковому пути.
Т.к. Друпал, все таки должен загрузиться, хотя бы на "ядерном" уровне.
Ну там, счетчик просмотров, сообщения системы, динамические блоки и пр.

Аватар пользователя pluser pluser 15 сентября 2008 в 1:10

Вообще вопреки сложившемуся мнению которое я заметил.
Друпал 6 визуально шустрее пашет чем 5 (даже если его ускорить).
Конечно исходя из тестов так не скажешь но визуально точно шустрее
но для этого надо подшаманить немого как я описал выше.

Аватар пользователя seaji seaji 15 сентября 2008 в 19:38

Честно говоря не очень представляю каким образом осуществить выборку сразу нескольких (но не всех) алиасов из базы данных.
Каким образом вы будете оформлять параметр WHERE ???
К тому же, придется затрагивать систему темизации. Я бы ее трогать не стал.
Как то очень сложно у Вас получается и еще не факт что вообще получится.

Аватар пользователя Akzhan Akzhan 17 сентября 2008 в 15:35

Всё просто.

Предположим, у нас Drupal 6, с двумя параметрами на запрос.

$wanted_subs = array();

function get_path_hash($path, $lang)
{
  return '%%%macro'. md5($path. ':'. $lang). '%%%';
}

lookup_path($path, $lang)
{
  // делаем так, чтобы лукапы к одному пути попадали в одну ячейку хэша, чтобы устранить дублирование. Хотя это и не обязательно.
  $hash = get_path_hash($path, $lang);
  $wanted_subs[$hash] = array('path' => $path, lang => $lang);
  return $hash;
}

after_render($html)
{
  $sql = 'SELECT ...AS result_path, ...AS source_path, ...AS lang FROM .. WHERE (0=1)';
  for ($wanted_subs as $hash => &$v)
  {
    // формируем список параметров для ...OR ((lang = $lang) AND (path = $path))
    $sql .= ...;
    $v = $v['path']; // теперь тут будущая подстановка по умолчанию.
  }
  $rows = drupal_db_query($sql);
  for ($rows as $row)
  {
    $hash = get_path_hash($row['source_path'], $row['lang']);
    $wanted_subs[$hash] = $row['result_path']
  }
  $what = array_keys($wanted_subs);
  $replace_with = array_values($wanted_subs);
  return str_replace($what, $replace_with, $html);
}

Как вы можете заметить - всего один запрос к БД.
И одна замена строк, даже без регулярок, причём одним вызовом.

Количество циклов минимально.

Аватар пользователя seaji seaji 15 сентября 2008 в 23:43

Замечен баг в первой версии этого патча.
Наблюдения показали, что некоторые ссылки принимают вид http://site/1 или http://site/save
Мне так мыслится, что не уничтожается переменная count - счетчик обновлений кеша и она может быть подставлена вместо какой либо ссылки.
Новый патч скоро выложу.

Аватар пользователя GogA GogA 16 сентября 2008 в 2:39

Интересный материал, спасибо.

Сначала отнёсся немного скептически, но когда увидел что количество запросов к БД сокращается почти в два раза, был удивлён.

Пока нет времени самому дорабатывать способ, буду ждать стабильный релиз.

Аватар пользователя Shedko Shedko 16 сентября 2008 в 16:49

А из-за чего могло появиться уйма одинаковых ссылок в меню ?

Т.е. ранее было например
Главная страница|О нас|Конфиденциальность|Пресс-центр|Форум|Обратная связь|Поиск

А теперь
Главная страница|Главная страница|Главная страница|Главная страница|Главная страница|Главная страница|Главная страница|О нас|Конфиденциальность|Пресс-центр|Форум|Обратная связь|Поиск

и не только в одном меню так, в нескольких местах выплыло это.
А не может ли это быть связано с тем, что на том сайте переопредела стартовая (frontpage) страница на отдельную страницу (frontpage_2008) ?

UPD: P.S.
А может ли этот вариант конфликтовать с модулем GlobalRedirect. Он все ссылки вида node/232 заменяет на алиасы.

UPD2: после отключения GlobalRedirect та же ситуация, но только для анонимомв. зарегистрированные видят ссылки как надо, а вот гостям выдается "туча" одинаковых ссылок на сайт/1

Аватар пользователя VladSavitsky VladSavitsky 16 сентября 2008 в 16:48

GlobalRedirect полезный модуль и нужно бы учесть его в работе модуля кеширования.
Он действительно позволяет одному системному пути иметь несколько синонимов и редиректит на основной.

Аватар пользователя seaji seaji 16 сентября 2008 в 22:18

Нет, модуль GlobalRedirect здесь не причем.
У меня таже ситуация. Этот баг я уже описал.
Причем по предварительным наблюдениям корябятся пункты меню, имеющие абсолютное значение
(включая http:// )
Поведение совершенно не объяснимое ни чем. Будем разбираться.
Всем, кто воспользовался моим патчем я рекомендую откатиться обратно к системному path.inc
Новую версию модуля я предполагаю сделать более похожей на "модуль". Все функции будут прошиты в нем.
В наиболее оптимальном варианте файл path.inc нужно будет хакнуть в одном месте.
Написать в начале функции drupal_lookup_path()
$map = get_alias_cache($map);

Аватар пользователя edhel edhel 17 сентября 2008 в 5:17

Эх, как бы всё можно сделать шустренько, если бы drupal был на java) Там и покэшировать можно было прямо в памяти, и соединение к БД постоянно держать, и статистику всякую писать в память, а сохранять отдельным потоком в БД параллельно обработке запросов, и java не критична на большие объемы кода, которые написаны, но не используются (но всё равно парсятся каждый раз в пхп).

Аватар пользователя Vladimir_VVV Vladimir_VVV 17 сентября 2008 в 7:18

edhel wrote:
Эх, как бы всё можно сделать шустренько, если бы drupal был на java) Там и покэшировать можно было прямо в памяти, и соединение к БД постоянно держать, и статистику всякую писать в память, а сохранять отдельным потоком в БД параллельно обработке запросов, и java не критична на большие объемы кода, которые написаны, но не используются (но всё равно парсятся каждый раз в пхп).

Почти все проблемы решает кеш php - берите серваки под дрю!

Аватар пользователя edhel edhel 17 сентября 2008 в 13:29

Почти все проблемы решает кеш php - берите серваки под дрю!

Да стоит eaccelerator... но на большом сайте с тысячами нод и обросшим большим кол-вом модулей всё равно шевелится не шустро. У нас генерация главной страницы 350-400 мс, новостная лента (views+cck) 900-1000 мс, статическая страница 300-350 мс, страница с большим кол-вом комментариев 1000 мс. При этом средняя загрузка процессоров (всего 4) 20-25%.

А на маленьком сайте со статическими страницами на том же серваке: статическая страница 130 мс.

Аватар пользователя Akzhan Akzhan 17 сентября 2008 в 22:53

Сейчас подробно изучил Drupal path/theme, в общем, я уже закодировал своё решение.

Выложу после тестирования.

В продложенном решении в статье, кстати, если недоработки. В первую очередь не отрабатывается корректно $action = 'wipe'.

Аватар пользователя Akzhan Akzhan 18 сентября 2008 в 0:34

А моё решение просто неработоспособно в текущей архитектуре Drupal.

Как минимум увидел проблему с модулем Global Redirect, надо вводить новые action в lookup_path.

Аватар пользователя seaji seaji 18 сентября 2008 в 2:51

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

Аватар пользователя Murz Murz 3 ноября 2008 в 23:10

При ускорении работы сайта на 5 версии Drupal я для себя открыл чудесную связку модулей Advanced cache http://drupal.org/project/advcache и Cache Router http://drupal.org/project/cacherouter - они как раз занимаются кешированием всего подряд, в том числе и алиасы путей. В качестве хранилища можно выбрать базу данных (эту же или другую), файл, APC cache, memcache, xcache и можно подключить ещё что угодно. Так что мне кажется правильнее будет допиливать этот модуль чем пытаться кешировать вручную какие-то элементы... Да и с обновлениями будет попроще....