Кэш Для Вики. Часть 2
Материал из DOM
Никто не будет спорить с тем, что mediawiki хоть и мощная система, однако и очень прожорливая. До недавнего момента этот факт меня совершенно не интересовал (посещаемость моего сайта в среднем не превосходит 150 человек в сутки, а время генерации страницы плавало около 1.5-2 секунд). Однако вот пришло ко мне письмо, где интересовались способами "разгона" mediawiki. Что же, задача актуальная и необходимая. К тому же я надеюсь, что посещаемость моего ресурса будет расти, и рано или поздно (а это всегда случается на виртуальных хостингах) мне должно будет придти письмо от провайдера, где попросят умерить аппетиты. Учитывая что дело было в субботу утром, и почти целый день был свободен, то я решил "стряхнуть пыль" с php и "слепить" что-нибудь такое этакое.
Собственно говоря, в mediawiki есть собственная система кэширования и устроена она сложно и хитро т.к. страницы Вики могут правиться кем-угодно, можно работать с историей правок, обсуждать документы. Меня это не устраивает т.к. правлю я свой сайт сам (горе-спамеры не в счет), правлю где-то раз, может два, в неделю. Таким образом количество просмотров (ах-да, у нас же есть google и прочие спайдер-мены) резко превалирует над количество правок и кэш можно сделать и проще и быстрее (читай без инициализации всех тех мегабайт wiki-кода которые будут проверять что-то в своем кэше, к тому же размещенном в БД). По сути, для кэша должно быть три операции: взять из кэша, положить в кэш и удалить из кэша. В качестве ключа кэша выступает адрес (имя страницы). Желательно при этом, чтобы хранилище кэша было максимально быстрым и отделено от хранилищ основной информации. Ведь кэшированная версия страницы (с html-кодом оформления) гораздо больше по размерам, чем голая информация хранимая в БД. Так что вскоре размер БД "зашкалит" за несколько сотен мегабайт и операции поиска и обновления такого кэша будут занимать время большее, чем генерация страницы "с нуля". Плюс проблемы с созданием backup-ов (если ваш тарифный план хостинга не позволяет иметь несколько БД).
Как вывод: хранить снимки страниц в mysql БД глупо. С другой стороны хранить данные в файловой системе еще глупее. Т.к. рано или поздно я захочу получить некоторую статистическую информацию об том, что там внутри кэша делается. Таким образом, на роль кэш-хранилища я выбрал sqlite 3 (об этой СУБД я уже рассказывал ранее в статье про google gears Разработка_веб-страниц_с_помощью_google_gears._Часть_2).
Как настроить?
1. Файл index.php в исталляции mediawiki я переименовал в index_core.php. Туда же, в корень инсталляции mediawiki, я поместил следующий скрипт и сохранил его под именем index.php.
2. Затем я создал папку XXX_CACHE в которой будут храниться кэш страниц (чтобы вы еще могли подумать?).
3. Настройка кода скрипта:
Константа PATH_TO_CACHE_DIR определяет путь к каталогу в котором будут храниться базы sqlite 3 и файлы журнала ошибок.
Константа FILE_NAME_SQLITE3 задает шаблон имени файла базы данных. Почему шаблон? Дело в том, что хранить все снимки в одной гигантской БД глупо. Sqlite, конечно продукт замечательный и всякое такое разное, но при внесении правок в БД требуется получить для нее монопольный режим доступа. Плюс, очевидно, что правка небольшого файла будет выполнена быстрее, чем большого. Как вывод, "снимки" страниц будут храниться в нескольких файлах. Имя файла будет вычисляться на основании имени страницы с помощью алгоритма crc32. Предельное количество подобных файлов задается константой CACHE_SPLIT_STRATEGY_SIZE. Имена баз данных формируется по правилу:
для шаблона mediawiki.cache.{0}.db3 например будут такие файлы: mediawiki.cache.1.db3, mediawiki.cache.2.db3, mediawiki.cache.3.db3 ...
Константа FILE_NAME_ERROR_LOG задает имя файла журнала ошибок (возникающих в ходе правок БД). Файл должен быть размещен внутри каталога PATH_TO_CACHE_DIR.
Последняя константа CACHE_STOLE_DELTA_MS задает время устаревания кэша (в ms.). Строго говоря, эту величину можно поставить бесконечно большой и тогда фактор времени будет исключен из правил устаревания информации в кэше.
До текущего момента, все настройки были "общими", т.е. этот кэш может быть с равной долей успеха применен к любому сайту не обязательно mediawiki. Теперь параметры специфические:
Константа URI_EXTRACTOR_FUNCTION_NAME задает имя функции которая играет роль "извлекателя" имени страницы из запроса.
Константа CUSTOM_PAGE_STRATEGY_SELECTOR_FUNCTION_NAME задает имя функции решающей по какой стратегии будет работать кэш. Принимая в качестве параметра "чистый" адрес запрошенной страницы, функция должна вернуть одно из трех значений:
- delete-from-cache страницу нужно удалить из кэша. - exclude страница не должна быть запрошена из кэша, а сгенерирована заново. - normal-flow работают стандартные правила кэширования.
Например, моя реализация функции получения стратегии, при запросе страницы на редактирование не берет ее из кэша. При отправке изменений в страницу, то информация об ней из кэша удаляется. И во всех остальных случаях используется стандартный life-cycle: посмотреть есть ли запись об таблице в кэше. Если ее нет или же время генерации страницы устарело, то сгенерировать страницу наново и поместить ее в кэш. В противном случае информация берется из кэша.
Таблица кэша имеет следующий вид:
CREATE TABLE IF NOT EXISTS wiki_cache ( id integer PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE, url varchar(1024) NOT NULL UNIQUE, md5url varchar(1024) NOT NULL UNIQUE, hitqty INT NOT NULL DEFAULT 0, refreshqty INT NOT NULL DEFAULT 0, refreshtimestamp INT NOT NULL, hittimestamp INT NOT NULL, refreshtimestamp_f TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, hittimestamp_f TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, content clob )
Как видите, я храню в в ней имя страницы, md5 свертку от ее имени (именно это поле, а не имя страницы используется при поиске записей в кэше). Также хранятся статистические сведения, такие как время попадания страницы в кэш, время последнего обращения к странице, количество запросов на обновление страницы (сколько раз ее ложили в кэш) и сколько раз страницу запрашивали. Все это может пригодиться для каких-то аналитических запросов. Последнее поле - content - хранит, собственно, содержимое документа.
Если вы откроете исходный html-код страницы, то в самом ее низу будет написано несколько слов о состоянии кэш системы:
<!-- got from sqlite3 cache ( timeused => 0.0780429840088; hitqty => 3; refreshqty => 1; hittimestamp => Sun, 21 Sep 08 15:08:46 +0300; refreshtimestamp => Sun, 21 Sep 08 15:08:46 +0300; comment => really got page from cache; used_db_name => mediawiki.cache.7.db3; md5url => 3609bfb8cef4042d33de5f20e6fb3842; ) -->
А теперь исходный код:
<?php // if not use cache uncomment next line //include ('index_core.php'); //die (); // ------------------- config ---------------------- define ('PATH_TO_CACHE_DIR' ,'КУДА ПОМЕСТИТЬ КЭШ'); // {0} means 'place here file number' (only for split strategy) define ('FILE_NAME_SQLITE3' ,'mediawiki.cache.{0}.db3'); // file name for error log file define ('FILE_NAME_ERROR_LOG' ,'errors.log'); // time in ms. to cache expire define (CACHE_STOLE_DELTA_MS, 1000 * 60 * 60 * 24 * 7 * 4); //define ('CACHE_STOLE_DELTA_MS', 1000); // if split stategy enabled then several files (sqllite3 databases) will be used to store cache // 0 -- fatal error define ('CACHE_SPLIT_STRATEGY_SIZE',10); // -------------------- excludes filters ------------------------- // for example input uri '/mediawiki/index.php?page=Bla-Bla-Bla&action=Boo' // will be transformed into "Bla-Bla-Bla" // function name (if you wish to skip this functionality then simple assign to this variable null) define ('URI_EXTRACTOR_FUNCTION_NAME', 'got_mediawiki_page_name_from_uri'); // function name which is responsible for custom processing urls: // each function receives as input arg "url" (and of course can use $_REQUEST super global array) // return value indicates which operation i must do with requested page // for example // - 'delete-from-cache' // - 'normal-flow' // - 'exclude' define('CUSTOM_PAGE_STRATEGY_SELECTOR_FUNCTION_NAME', 'got_strategy_for_mediawiki'); // ---------------------------- *********************** ------------------------ // ---------------------------- end of config variables ------------------------ // ---------------------------- end of config variables ------------------------ // -------------------- helper functions ----------------- if (! function_exists('getmicrotime')){ function getmicrotime() { list($usec, $sec) = explode(" ", microtime()); return ((float)$usec + (float)$sec); } } // test if this url is 'special' & must be excluded from processing function skipThisUrl ($url){ global $excludes; for ($i = 0; $i < count($excludes); $i++) if (preg_match($excludes[$i], $url)) return true; return false; } // extrcats page name ('Page Title') from requested uri function got_mediawiki_page_name_from_uri ($uri){ $pos_act = strpos($uri, 'title='); if ($pos_act === false){ $pos_sep = strrpos($uri, '/'); if ($pos_sep !== false) return substr($uri, $pos_sep + 1); else return $uri; } return $_REQUEST['title']; } //selects cache strategy for page function got_strategy_for_mediawiki ($clean_uri){ // - delete-from-cache // - normal-flow // - exclude if (stripos($clean_uri, 'Rss?g')!== false) return 'exclude'; $action = isset($_REQUEST['action'])?$_REQUEST['action']:null; if ($action === 'edit') return 'exclude'; if ($action === 'history') return 'exclude'; if ($action === 'talk') return 'exclude'; if ($action == 'submitlogin') return 'exclude'; if ($action == 'submit') return 'delete-from-cache'; return 'normal-flow'; } function ready_content($content, $refreshtimestamp, $hittimestamp, $refreshqty ,$hitqty,$comment){ // script statup time global $SCRIPT_START_TIME; global $url; global $used_db_name; // row object for case 'cache contains page data' global $__cache2db_lastrow; $details = array ( 'timeused' =>getmicrotime() - $SCRIPT_START_TIME, 'hitqty' => $__cache2db_lastrow['hitqty'], 'refreshqty' => $__cache2db_lastrow['refreshqty'], 'hittimestamp' => date(DATE_RFC822, $__cache2db_lastrow['hittimestamp']), 'refreshtimestamp' => date(DATE_RFC822, $__cache2db_lastrow['refreshtimestamp']), 'comment' => $comment, 'used_db_name' => $used_db_name, 'md5url' => md5($url) ); $sdetails = ''; foreach ($details as $k => $v) $sdetails .="\t". $k . ' => ' . $v .";\n"; return ($content . "<!-- got from sqlite3 cache (\n".$sdetails.") -->"); } // logs info about error while executing sqlite 3 code function logSq3Error ($e){ $fname = PATH_TO_CACHE_DIR . FILE_NAME_ERROR_LOG; $h = fopen ($fname , 'a+'); fwrite($h, date (DATE_RFC822). ' -- ' . $e . "\n"); fclose($h); } if (! function_exists('dexport')){ function dexport ($x){ die (var_export ($x, true) ); } } function safe_query ($conn, $sql){ if (! $conn->query ($sql)) dexport ($conn->errorInfo() ); } // inserts new record into db function doInsert ($hitqty, $refreshqty, $content){ global $url; global $conn; global $NOW; // insert new record in cache table $stmt = $conn->prepare("INSERT INTO wiki_cache (url, md5url,refreshtimestamp ,hittimestamp, hitqty, refreshqty, content) VALUES (:url, :md5url, :refreshtimestamp ,:hittimestamp, :hitqty, :refreshqty, :content) ") ; $stmt->bindValue(':url', $url, PDO::PARAM_STR); $stmt->bindValue(':md5url', md5($url), PDO::PARAM_STR); $stmt->bindValue(':hittimestamp', $NOW, PDO::PARAM_INT); $stmt->bindValue(':refreshtimestamp', $NOW, PDO::PARAM_INT); $stmt->bindValue(':hitqty', $hitqty, PDO::PARAM_INT); $stmt->bindValue(':refreshqty', $refreshqty, PDO::PARAM_INT); $stmt->bindValue(':content', $content, PDO::PARAM_LOB); if (!$stmt->execute()){ $e = $stmt->errorInfo(); logSq3Error ($e[2]); //@@@ HACK ONLY FOR ME //return false; return strval($e[2]); } return false; } // updates record in db function doUpdate (){ global $url; global $conn; global $NOW; $stmt = $conn->prepare("UPDATE wiki_cache SET refreshtimestamp = :refreshtimestamp, hittimestamp = :hittimestamp, content = :content, hitqty = 1 + hitqty, refreshqty = 1+ refreshqty, hittimestamp_f = CURRENT_TIMESTAMP, refreshtimestamp_f = CURRENT_TIMESTAMP WHERE md5url = :md5url") ; $stmt->bindValue(':md5url', md5($url), PDO::PARAM_STR); $stmt->bindValue(':hittimestamp', $NOW, PDO::PARAM_INT); $stmt->bindValue(':refreshtimestamp', $NOW, PDO::PARAM_INT); $stmt->bindValue(':content', $content, PDO::PARAM_LOB); if (!$stmt->execute()){ $e = $stmt->errorInfo(); logSq3Error ($e[2]); //@@@ HACK ONLY FOR ME //return false; return strval($e[2]); } return false; } function doDelete (){ global $url; global $conn; global $NOW; $stmt = safe_prepare($conn, "DELETE FROM wiki_cache WHERE md5url = :md5url") ; $stmt->bindValue(':md5url', md5($url), PDO::PARAM_STR); if (!$stmt->execute()){ $e = $stmt->errorInfo(); logSq3Error ($e[2]); //@@@ HACK ONLY FOR ME //return false; return strval($e[2]); } return false; } function safe_prepare ($conn, $sql){ $c = $conn->prepare ($sql); if (! $c ) dexport ($conn->errorInfo() ); return $c; } function get_page_row_from_cache ($url){ global $conn; $stmt = safe_prepare($conn, "SELECT url, refreshtimestamp, hittimestamp, hitqty, refreshqty FROM wiki_cache WHERE md5url = :md5url"); $stmt->bindValue(':md5url', md5($url), PDO::PARAM_STR); $stmt->execute(); $row = $stmt->fetch(PDO::FETCH_ASSOC); return $row; } function initializeSQL3Connection(){ global $url; if (CACHE_SPLIT_STRATEGY_SIZE < 0) die ('CACHE_SPLIT_STRATEGY_SIZE must be greater then zero (0)'); $c_file_num = '-'; if (CACHE_SPLIT_STRATEGY_SIZE > 1) $c_file_num = abs(crc32($url) % CACHE_SPLIT_STRATEGY_SIZE); global $used_db_name; $used_db_name = str_replace('{0}',$c_file_num , FILE_NAME_SQLITE3); $sqlurl = 'sqlite:' . PATH_TO_CACHE_DIR . $used_db_name ; $conn = new PDO($sqlurl); return $conn; } //function stores page content in DB function __cache2db_persister ($content){ // connection to sqllite global $conn; // requested url global $url; // timestamp on script startup global $NOW; // info about service availability & requested operation global $__cache2db_operation; global $__cache2db_unvailable; // row object for case 'cache contains page data' global $__cache2db_lastrow; // if sqllite is unvailable then skip if ($__cache2db_unvailable == true){ return $content; } if ($__cache2db_operation == 'insert'){ // insert new record $status = doInsert(1, 1, $content); if ($status) return $status; //save stat info about current request $__cache2db_lastrow = array ('refreshqty' => 1, 'hitqty' => 1, 'refreshtimestamp' => 1, 'hittimestamp' => 1); return ready_content ($content, $NOW, $NOW, 1 , 1, 'put page into cache'); } // if operation is 'update' // using UPDATE starteggy or DELETE INSERT // i have strage bug when for update operation table is locked by unknown process if (true){ $status = doUpdate(); if ($status) return $status; } else{ $status = doDelete(); if ($status) return $status; $status = doInsert($__cache2db_lastrow['hitqty']+1, $__cache2db_lastrow['refreshqty']+1); if ($status) return $status; } return ready_content ($content, $NOW, $NOW, $__cache2db_lastrow['refreshqty']+1 , $__cache2db_lastrow['hitqty']+1, 'cache was refreshed'); }// end -- FOO -- // ------------------------------------- // ------------------------------------- LET'S START ROCK ------------------------- // info about requested operation $__cache2db_unvailable = false; $__cache2db_operation = false; $__cache2db_lastrow = false; // now timestamp $NOW = time (); $SCRIPT_START_TIME = getmicrotime(); // path to dir where cache sqllite3 databsse will be stored $used_db_name = null; if (!file_exists(PATH_TO_CACHE_DIR)){ mkdir (PATH_TO_CACHE_DIR, 0777, true); } $what_is = array ('REQUEST_URI', 'REDIRECT_QUERY_STRING', 'REDIRECT_URL' , 'argv[0]'); $url = null; for ($i = 0 ; $i < count($what_is); $i++) if (isset($_SERVER[$what_is[$i]])){ $url = $_SERVER[$what_is[$i]]; break; } if (URI_EXTRACTOR_FUNCTION_NAME != null) $url = call_user_func(URI_EXTRACTOR_FUNCTION_NAME, $url); $strategy = 'normal-flow'; if (CUSTOM_PAGE_STRATEGY_SELECTOR_FUNCTION_NAME != null) $strategy = call_user_func(CUSTOM_PAGE_STRATEGY_SELECTOR_FUNCTION_NAME, $url); // - delete-from-cache // - normal-flow // - exclude $conn = null; if ($strategy == 'exclude') { $url = false; } if ($strategy == 'delete-from-cache'){ // first delete page from cache then exclude page from cache $conn = initializeSQL3Connection (); doDelete(); $url = false; } if (! $url){ $__cache2db_unvailable = true; } else{ if (skipThisUrl($url)){ // skip current page from processing $url = false; } else{ $conn = initializeSQL3Connection (); } } //$conn->beginTransaction(); if (! $conn || ! $url){ $__cache2db_unvailable = true; } else{ $MD5URL = md5($url); $CREATE_TABLE_SQL = 'CREATE TABLE IF NOT EXISTS wiki_cache ( id integer PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE, url varchar(1024) NOT NULL UNIQUE, md5url varchar(1024) NOT NULL UNIQUE, hitqty INT NOT NULL DEFAULT 0, refreshqty INT NOT NULL DEFAULT 0, refreshtimestamp INT NOT NULL, hittimestamp INT NOT NULL, refreshtimestamp_f TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, hittimestamp_f TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, content clob );'; safe_query ($conn, $CREATE_TABLE_SQL); // if got url then start test db $row = get_page_row_from_cache ($url); if (! $row){ // if cache contains no required page $__cache2db_operation = 'insert'; } else{ // if row exists i cache $__cache2db_lastrow = $row; if ( ($NOW - $row['timestamp']) > CACHE_STOLE_DELTA_MS){ // update cache by new value $__cache2db_operation = 'update'; } else{ // got value from cache & return it $__cache2db_operation = 'select'; } }// if row exists }// if sqlite available // now complete each case if ($__cache2db_operation == 'select'){ // got value from cache & return it $stmt = $conn->prepare("UPDATE wiki_cache SET hittimestamp = :hittimestamp, hitqty = 1 + hitqty, hittimestamp_f = CURRENT_TIMESTAMP WHERE md5url = :md5url") ; $stmt->bindValue(':md5url', md5($url), PDO::PARAM_STR); $stmt->bindValue(':hittimestamp', $NOW, PDO::PARAM_INT); $stmt->execute(); $stmt = $conn->prepare("SELECT hitqty, refreshtimestamp, refreshqty, content FROM wiki_cache WHERE md5url = :md5url"); $stmt->bindValue(':md5url', md5($url), PDO::PARAM_STR); $stmt->execute(); $row = $stmt->fetch(PDO::FETCH_ASSOC); die (ready_content ($row['content'], $row['refreshtimestamp'], $NOW, $row['refreshqty'] , $row['hitqty'], 'really got page from cache')); } // register special 'save page in db' function ob_start ('__cache2db_persister'); // include mediawiki code include ('index_core.php'); // flush cache ob_end_flush(); //$conn->commit(); ?>
|
|
Subscribe Now! |
|
