Сьогодні ми знову пропонуємо вам блог одного з наших Drupal-розробників, а це означає багато важливих деталей та практичних порад — цього разу щодо кешування в Drupal 7. Сьома версія Друпала зберігає свої позиції, тож багато розробників обирають її для побудови сайтів, та й для написання блогів ;) У нас є статті про створеннія CTools попапів, роботу з модулем Book, побудову додатків з PhoneGap, важливі інструменти для розробника в Drupal 7, налаштування пошуку з ApacheSolr і багато іншого в рубриці програмування. Але просто зараз час зануритися в тонкощі кешування! ;)
Кеш — це інструмент веб-розробника, що дозволяє користувачеві отримати запрошені дані з максимальною швидкістю і з найменшими системними затратами. По суті це проміжний буфер, в якому зберігаються дані, до яких часто звертаються. Як на практиці реалізується цей буфер? Зазвичай він являє собою таблицю в базі даних з уже готовими даними, або замість таблиці часто використовується збереження даних у файлі.
Підкріплюючи вищесказане, можна навести простий приклад використання кешування. Припустимо, що на на певну сторінку сайту часто заходять нові користувачі і для кожного з цих користувачів скрипт на сторінці мусить виконати ряд операцій, які в результаті займають певний час (до прикладу візьмемо 30 секунд). І лише після виконання цих обрахунків користувач бачить на екрані потрібний результат.
Виходить, що будь-який раз, коли користувач заходить на сторінку, він чекає 30 секунд, аби отримати бажаний результат. Проте, скажете ви, чому б після першого виконання функції, результат її обробки не зберегти і всім наступним просто показувати його, оминаючи довге виконання функції? Себто дані 30 секундного виконання ми просто запишемо у вище описаний "проміжний буфер" і щоразу будемо миттєво їх віддавати користувачу — це і є загальна філософія кешування.
В Друпал існує ряд уже готових інструментів, що дозволяють маніпулювати даними та кешувати їх за певними алгоритмами. Саме ці інструменти та способи їх застосування ми і розглянемо нижче.
Як говорилось, кеш Друпала зберігається в таблиці бази даних. Проте наша система керування контентом розподіляє кеш і ділить її на певні сегменти, кожен з яких представляє в базі свою окрему таблицю з індивідуальною назвою. Чому відбулась ця сегментизація, та які переваги ми отримуємо від такої системи реалізації? Розглянемо ці сегменти та наведемо приклади практичного їх застосування.
Сегменти кешу
Стандартно кожен сегмент складається з 5 полів:
$cid — кеш ідентифікатор (ID) даних в сховищі. Можна використовувати будь-яку послідовність символів довжиною не більше 255. Рекомендовано починати ідентифікатор з назви модуля.
$data — з назви зрозуміло, що тут якраз і будуть зберігатись наші дані. Складні типи даних будуть автоматично серіалізуватись.
$expire — (optional) Час життя кешу. Може бути наступних значень:
- CACHE_PERMANENT: вказує, що дані ніколи не повинні бути видалені. Крім випадку коли явно вказане ID того елемента при виклику функції cache_clear_all().
- CACHE_TEMPORARY: вказує, що елемент повинен бути видалений при наступному загальному очищенні кешу.
- UNIX — мітка часу: вказує час життя елементу в UNIX форматі. Після чого він веде себе як CACHE_TEMPORARY.
$created — час збереження кешу в Unix форматі.
$serialized — прапорець (0 або 1), що вказує, були(1) чи ні(0) кешовані дані серіалізовані. Існують такі функції для роботи з кешем.
cache_clear_all() — очищує дані з кешу.
cache_get() — повертає дані з кешу.
cache_get_multiple() — повертає дані з кешу, з масиву ідентифікаторів кешу.
cache_is_empty() — перевіряє, чи є кеш порожній.
cache_set() — зберігає дані в кеші.
_cache_get_object() — отримує об'єкт кешу по сегменту($bin).
Наприклад:
function cache_set($cid, $data, $bin = 'cache', $expire = CACHE_PERMANENT) { return _cache_get_object($bin)->set($cid, $data, $expire); }
Вертаючись до сегментів:
1. {cache} — (default) сегмент для зберігання загального кешу. Зазвичай використовується для реєстру теми, локальних дат, списків SimpleTest тестів і т.д. Або для даних, класифікація яких неможлива, чи створення для них окремого сегменту не має сенсу. Часто використовується для кешування даних з кастомних модулів.
2. {cache_block} — додається при включенні модуля Block. Працює наступним чином — коли відбувається завантаження регіону теми, проводиться завантаження даних по всіх блоках і перевірка на предмет побудови його з кешу, само собою хук hook_block_view() виконуватись не буде, якщо кеш для блоку заданий. Запит до {cache_block} вертає уже відрендерений блок. Не варто забувати і про параметри кешування при створення блоку через hook_block_info(), де опційно можна задати один з наступних параметрів:
- DRUPAL_CACHE_PER_ROLE (за замовчуванням): Блок може змінюватися в залежності від ролі користувача і наданим йому прав.
- DRUPAL_CACHE_PER_USER: Блок може змінюватися в залежності від користувача при перегляді сторінки. Проте цей параметр може бути ресурсомістким для сайтів з великою кількістю користувачів, і його слід використовувати тільки тоді, коли DRUPAL_CACHE_PER_ROLE недостатньо.
- DRUPAL_CACHE_PER_PAGE: Блок може змінюватися в залежності від сторінки що переглядається.
- DRUPAL_CACHE_GLOBAL: Блок є однаковим для кожного користувача на кожній сторінці, де його видно.
- DRUPAL_CACHE_CUSTOM: Модуль реалізує свою власну систему кешування.
- DRUPAL_NO_CACHE: Блок не повинен кешуватися. Ну і при створенні блоку йому виставляється прапорець CACHE_TEMPORARY, тобто дані будуть видалені при наступному загальному очищенні кешу.
3. {cache_bootstrap} — даний сегмент кешу відповідає за дані ініціалізації Drupal (списки модулів, змінних, хуків включених в системі і тд.) Для даного сегмента існують наступні ідентифікатори ($cid) даних в сховищі:
- bootstrap_modules — список модулів, які імплементують хуки початкового завантаження (завантажуються раніше інших модулів).
- hook_info — список всіх хуків, що доступні для імплементації.
- lookup_cache — список класів, що динамічно завантажуються, та файлів, в яких вони зберігаються. module_implements — список хуків разом з модулями, в яких є реалізація цих хуків.
- system_list — список увімкнених модулів і тем разом з залежностями.
- variables — кеш змінних (таблиця "variables").
4. {cache_field} — в даному сегменті зберігаються дані по всіх полях. ID ($cid) формується за правилом field:тип_сутності: id_сутності.
Наприклад, для полів таксономії з tid рівним 14 : field:taxonomy_term:14
Тут зберігається набір всіх полів та їх значень, що відносяться до даної сутності.
5. {cache_filter} — сегмент, створений модулем Filter(ядро), що відповідає за фільтрацію текстових форматів. Тобто для текстів, можна створити уже готовий кеш по його MD5 хешу. Наприклад, функція check_markup() по SHA-2 хешу тексту перевіряє, чи є в кеші сегменту {cache_filter} уже відфільтрований текст, і при позитивному результаті вертає його. Якщо відфільтрованого тексту в кеші немає, то завантажується список включених фільтрів поточного формату введення і текст буде оброблятись згідно налаштувань фільтру, що веде за собою певну затрату часу. Тому по можливості викликаючи check_markup(), варто користуватись кешуванням.
6. {cache_form} — сегмент кешування побудови форм. За своєю логікою відрізняється від вищеописаних сегментів і застосовується не для збільшення продуктивності, а швидше для безпеки і уникнення виникнення вразливостей при побудові форм з допомогою FORM API. Ця таблиця має властивість стрімко рости (час життя 6 год вказано в ядрі includes/form.inc), тому якщо на сайті чимало форм плюс велика відвідуваність, варто подумати над налаштуванням крону (чи інших кастомних рішень) для періодичного очищення даного сегменту.
7. {cache_image} — сегмент зарезервований модулем Image. Як такого кешу зображень в ньому нема і в основному таблиця служить для зберігання даних про маніпуляції з картинками.
8. {cache_menu} — сегмент, зарезервований модулем Menu, що служить для кешування і зберігання інформації про посилання зі всіх меню, створених через інтерфейс Drupal. Сам ідентифікатор кешу формується наступним чином:
$cid = 'links:' . $menu_name . ':tree-data:' . $GLOBALS['language']->language . ':' . hash('sha256', serialize($parameters));
Наприклад:
links:main-menu:page:admin/reports/status:ru:1:1
9.{cache_page} — один з найважливіших сегментів кешування. Служить для зберігання кешованих сторінок для анонімних користувачів. У випадку, коли при побудові сторінки для такого користувача був знайдений кеш, виконається лише 2 хуки hook_boot() и hook_exit(), всі решта буду пропущені. Для включення цього кешу достатньо перейти admin/config/development/performance і включити відповідне кешування.
10. {cache_path} — сегмент, що відповідає за збереження відповідностей між системним шляхом та його аліасом.
11. {cache_update} — сегмент, резервований модулем Update manager. В даних кешу зберігається вся інформація по релізах для увімкнених модулів. Проте, сегмент виступає більше як таблиця збереження даних і на продуктивність не впливає. Обновлення таблиці відбувається лише у випадку отримання Drupal нових даних щодо релізів модулів.
Також додаткові сегменти створюють сторонні модулі (hacked, l10n_update, token, views). Серед них окремо варто згадати {ctools_object_cache} — сегмент кешу, що служить для зберігання великих об’єктів, що редагуються в даний момент. Саме тому замість поля $cid в цій таблиці знаходиться $sid(Session ID) — ідентифікатор поточної сесії користувача. Так само таблиця не має $expire, тому не стирається після очищення кешу через інтерфейс, проте чиститься раз в добу по крону. Прикладом може бути кеш сконфігуреного, та ще не збереженого views.
Узагальнюючи вище викладене, можна сказати, що сегмент кешування не завжди являє собою саме той кеш в розумінні буфера, про що говорилось з самого початку. Тому, знаючи кожен сегмент і його призначення, можна вміло володіти цим інструментом та вдало застосовувати на практиці.
Створюємо кастомні сегменти
А як щодо створення власного сегменту для індивідуальних потреб, наприклад, кастомного модуля? В цій ситуації нам треба буде просто клонувати стандартний сегмент {cache} і на його основі створити свій власний. Наприклад, створимо модуль “My module”, для якого “cache_my_module” буде нашим новим сегментом кешування.
Щоб клонувати таблицю cache, в my_module.install імплементуємо hook_schema() і додаємо свою таблицю.
/** * Implements hook_schema(). */ function my_module_schema() { $schema['cache_my_module'] = drupal_get_schema_unprocessed('system', 'cache'); $schema['cache_my_module']['description'] = 'Cache table for My module.'; return $schema; }
Все — таблиця створена. Далі важливим кроком буде передбачити очищення таблиці при системному очищенні кешу. Для цього в Drupal існує ось такий хук — hook_flush_caches(). Застосуємо його. Для цього в файлі my_module.module пишемо:
/** * Implements hook_flush_caches(). */ function my_module_flush_caches() { return array('cache_my_module'); }
На прикладі функції нижче показано використання кешу. Спеціально створено ресурсомісткий цикл, який вираховує суму за певним правилом. При першому запуску ця функція виконалась за 5.85 с., іншим же разом і всіма наступними разами — за приблизно 0.001с.
/** * Example function. */ function my_module_cache_page() { timer_start('cache_module'); $cache_id = 'cache_example_files_count'; $sum = 0; if ($cache = cache_get($cache_id, 'cache_my_module')) { //якщо кеш по ідентифікатору є, тоді присвоюємо йому дані $sum = $cache->data; } else { //проводимо деякі ресурсомісткі обрахунки for ($i = 0; $i < 30; $i++){ $files_count = count(file_scan_directory('.', '/.*/')); $sum += $files_count * $i; } //записуємо наші обрахунки в кеш cache_set($cache_id, $sum, 'cache_my_module', REQUEST_TIME + 15 * 60); } //виводимо затрачений час $timer = timer_read('cache_module') / 1000; print $sum . ' We got a sum in = ' . $timer . 'seconds.'; }
Як видно, різниця досить значна. Однак, при значних навантаженнях на сайт варто зосередити увагу на мінімізації звернень до бази даних. А тому постає питання про перенесення кешу за межі БД.
Винесення сегментів за межі БД
Зробимо це з допомогою, наприклад, Memcached. Memcached в першу чергу має бути встановлений на сервері. Встановити треба або самому, якщо це виділений сервер, або попросити в хостера. Далі нам необхідно буде перенести всі сегменти кешування. Для цього нам допоможе уже готовий модуль Memcache Storage. Перед встановленням треба зв’язати функціонал модуля з демоном Memcached на сервері. Робиться це додавання наступних рядків коду у settings.php:
$conf['cache_backends'][] = 'sites/all/modules/memcache_storage/memcache_storage.inc'; $conf['cache_default_class'] = 'MemcacheStorage';
Тепер можна увімкнути модуль. Та слід зауважити, що Memcached працює, використовуючи оперативну пам’ять. А ми пам’ятаємо про сегмент кешу {cache_form}, що може бути дуже значний в розмірах, тим паче при великій кількості користувачів. Виходить, ці дані зберігати в оперативній пам’яті буде не зовсім коректно. Тому наступними рядками скажемо Друпалу, щоб кеш даного сегменту повернув нам назад в базу:
$conf['cache_class_cache_form'] = 'DrupalDatabaseCache';
Існує ще кілька розповсюджених практик ефективного використання Memcached. Оскільки ми і так перенесли кеш в оперативну пам’ять, сенсу звернення Друпала до бази при побудові сторінки на фазі бустрапа нема, тому:
// відправляємо кеш сторінки в оперативну пам’ять $conf['cache_class_cache_page'] = 'MemcacheStorage'; // деактивуємо підключення до БД $conf['page_cache_without_database'] = TRUE;
Крім того, існують функціонали, що напряму зачіпають БД з кешем, так як на стадії бустрапу все одно викликаються хуки hook_boot() і hook_exit(). Поправимо і це:
$conf['page_cache_invoke_hooks'] = FALSE;
Крім Memcached можна використовувати інші рішення. Наприклад APC, Boost, Varnish, XСache, Redis. І всі ці системи можна використовувати одночасно, виносячи певні сегменти кешу в те чи інше рішення.
Уcпіху в роботі з сегментами кешу та кешуванням у Drupal 7!