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

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

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

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

Вот ссылки по теме:
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 КБ

Комментарии

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

14 сентября 2008 в 23:53

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

15 сентября 2008 в 0:11

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

15 сентября 2008 в 0:17

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

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

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

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

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

15 сентября 2008 в 0:38

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

15 сентября 2008 в 0:59

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

15 сентября 2008 в 1:10

поле в таблицы ноды совсем не решение проблемы, ибо множество алиасов существует еще и помимо нодов

15 сентября 2008 в 0:37

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

15 сентября 2008 в 19:38

Всё просто.

Предположим, у нас 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);
}

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

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

17 сентября 2008 в 15:35

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

15 сентября 2008 в 23:43

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

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

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

16 сентября 2008 в 2:39

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

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

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

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

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

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

16 сентября 2008 в 16:49

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

16 сентября 2008 в 16:48

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

16 сентября 2008 в 22:18

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

17 сентября 2008 в 5:17

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

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

17 сентября 2008 в 7:18

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

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

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

17 сентября 2008 в 13:29

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

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

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

17 сентября 2008 в 22:53

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

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

18 сентября 2008 в 0:34

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

18 сентября 2008 в 2:51

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

3 ноября 2008 в 23:10