УрокСистема кэширования Drupal 7. Часть первая: сегменты кэша.

По опыту начал замечать, что многие разработчики, особенно junior/mid уровня, имеют довольно слабое представление о системе кэширования Друпала. До сих пор ни один разработчик не ответил мне правильно на все вопросы о хранилищах и назначении стандартного кэша, который включается в ядро Друпала уже достаточно давно. Мне кажется, уже пора раз и навсегда закрыть этот вопрос.

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

Кэш Друпала хранится не в одном единственном месте, а разбит по сегментам. По умолчанию в Друпале каждый сегмент кэша находится в базе данных в виде таблиц под определённым именем. Это сделано по нескольким причинам:

  • Во-первых, это позволяет выносить определённые сегменты кэша в другие системы хранения кэша (например, в Memcached или Boost). Некоторые сегменты кэша обновляются чаще, другие реже. Это стоит учитывать при разработке сайта, и пробовать разные системы хранения кэша.
  • Во-вторых, это повышает производительность работы с кэшем – в меньших объёмах данных работа с записями происходят быстрее. Более того, при определённых действиях кэш может очищаться частично (сегментно). Согласитесь, в базе данных гораздо быстрее выполнится полная очистка таблицы (TRUNCATE), нежели частичное удаление при помощи %LIKE% .

На текущий момент (Drupal 7.15) ядро Друпала вместе со стандартными модулями насчитывает 11 различных сегментов кэша. Предлагаю разобрать каждый из них подробно.

Общее хранилище кэша.

{cache} - сегмент для общего хранилища кэша. Сюда попадают данные, которые невозможно классифицировать, либо же нет смысла создавать под них новый сегмент кэша. По большому счёту, это своего рода сток различных данных кэша.

Этот сегмент состоит из 5 полей:

  • cid (Cache ID)- уникальный идентификатор кэша. Сюда обычно записывается любая последовательность символов, по которой вы сможете идентифицировать запись. Например, если бы вы написали свой модуль, который кэширует данные для ноды с nid 74, то можно было бы cid выставить как module_name:node:74. Строгих правил для заполнения этого поля нет, однако довольно удобно (рекомендуется) начинать его с названия модуля, который сохраняет кэш. Единственный нюанс - длина не должна превышать 255 символов.
  • data - данные, которые должны быть закэшированы. Здесь нет ограничений по размеру записываемых данных. Поэтому всё, что должно быть сохранено для быстрого доступа, может быть записано в эту таблицу.
  • expire - время в Unix формате, которое указывает окончание "срока годности" кэша. Например, если вы хотите выставить кэш на 20 минут, то делается это так:
  •  cache_set('module_name:cache_cid', $data, REQUEST_TIME + 60 * 20);
     

    По сути, к текущему времени просто добавляется 20 минут (1200 секунд в Unix формате), и это время записывается в поле expire.

  • created - время сохранения кэша в Unix формате.
  • serialized - флаг (0 или 1), показывающий, были ли кэшируемые данные сериализованы. Напомню, что простые типы данных (строки, числа, null) в сериализации не нуждаются.

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

В общий сегмент кэша могут закидываться данные не только из ядра Друпала, но и из контриб-модулей.

Кэш блоков

Сегмент {cache_block} добавляется при включении модуля Block (входит в ядро).

При загрузке региона Друпал загружает данные по всем блокам этого региона. В этот момент проверяется, а не закэшированы ли данные для блока. Если так и есть - то делается запрос к сегменту {cache_block} и вытаскивается массив с данными блока (уже отрендеренными), тем самым пропуская вызов хука hook_block_view() для этого блока с его последующим рендером и с его последующим кэшированием.

Здесь стоит отметить, что кэширование для блоков отключается, если включаются модули по работе с доступами к материалу, а конкретнее - имплементация hook_node_access().

При создании блока через hook_block_info() можно управлять параметрами кэширования для блока, указав ему одну из констант:

  • DRUPAL_CACHE_PER_ROLE (по умолчанию): Блок кэшируется в зависимости от роли пользователя, который просматривает страницу.
  • DRUPAL_CACHE_PER_USER: Блок кэшируется для каждого пользователя индивидуально. Обратите внимание, что для сайтов с большим количеством пользователей выставление этого флага будет создавать большое количество данных в кэшируемой таблице.
  • DRUPAL_CACHE_PER_PAGE: Блок кэшируется в зависимости от страницы, на которой он отображается.
  • DRUPAL_CACHE_GLOBAL: Блок кэшируется для всех страниц и всех пользователей, вне зависимости от внешних факторов.
  • DRUPAL_NO_CACHE: Блок не кэшируется вообще. Это значит, что каждый раз при загрузке региона, вызывается загрузка содержимого блока с последующим его рендером.

При сохранении кэша блока ему выставляется флаг CACHE_TEMPORARY. Это означает, что кэш блока будет стёрт при ближайшем вызове функции очистки кэша.

Кэш загрузки (бутстрапа)

{cache_bootstrap} - сегмент кэша, в котором хранятся данные, инициализируемые при загрузке Друпала. Вам лично вряд ли доведётся с ней работать, однако знать что в ней хранится, я считаю, надо обязательно (далее жирным выделен Cache ID кэша):

  • bootstrap_modules - список модулей, которые имплементируют хуки бутстрапа. Они загружаются раньше остальных модулей.
  • hook_info - список всех хуков, доступных для имплементации.
  • lookup_cache - данные, которые предоставляют список динамически загружаемых классов и файлов, в которых они хранятся.
  • module_implements - данные, предоставляющие список хуков вместе с модулями, в которых есть реализация этих хуков.
  • system_list - список включенных модулей и тем, со всеми зависимостями, подключенными интерфейсами и так далее. То есть по сути, тут хранятся структурированные данные из .info файла.
  • variables - кэш переменных, записанных в таблицу {variables}. При бутстрапе Друпал загружает в глобальную переменную $conf из кэша список всех доступных переменных. Этот трюк позволяет существенно ускорить выполнения множественных вызовов функции variable_get(), т.к. результат не загружается каждый раз из базы данных, а из оперативной памяти забирается уже сохранённое значение.

Кэш полей

Данные по всем полям (fields) хранятся в сегменте {cache_field}. Он добавляется при включении модуля Field.

В каждой записи кэша хранится набор полей со значениями, которые относятся к загружаемой сущности. При программной загрузке сущности, из этой таблицы забирается кэш всех полей сущности и добавляется к загруженному объекту. Если же кэша нет в таблице, то для каждого поля вызывается загрузка информации и данных, после чего полученный результат сохраняется в сегменте {cache_field}.

Cache ID формируется по правилу field:тип_сущности:id_сущности. Например, для материала с nid 52 Cache ID будет выглядеть как field:node:52.

Кэш фильтров

С модулем Filter сталкивались все – он входит в набор модулей ядра, которые обязательно включаются при установке Друпала. Этот модуль создает свою таблицу для хранения кэша для обработанного фильтрами текста. Например, при вызове check_markup() вы можете передать четвёртый параметр TRUE, и тогда текст после прохождения фильтра будет сохранён в сегменте {cache_filter}. В обратном случае фильтр будет вызываться каждый раз, обрабатывая один и тот же текст при каждой его загрузке. Чем сложнее фильтр, тем больше процессорного времени тратится на обработку текста. Поэтому по возможности я бы рекомендовал все вызовы check_markup() кэшировать.

При сохранении (обновлении) и отключении фильтра, все его сохранённые данные будут удалены из сегмента с кэшем.

Cache ID для таблицы {cache_filter} собирается по правилу название_формата:язык:хэш_текста :

$cache_id = $format->format . ':' . $langcode . ':' . hash('sha256', $text);

Кэш форм

На самом деле, это не совсем тот кэш, который содержится в остальных хранилищах кэша. Если остальные кэшируемые данные хранятся для ускорения работы сайта, то этот кэш на производительность никак не влияет. Однако благодаря ему все формы, построенные с помощью Forms API Друпала, являются абсолютно безопасными с точки зрения уязвимостей.

Каждый раз при построении формы она сохраняется в сегменте {cache_form}. Причем сама форма и массив с её значениями хранятся отдельно - у них Cache ID field_$form_build_id и field_state_$form_build_id соответственно. При нажатии на сабмит Друпал проверяет полученные POST/GET данные с теми, что были сохранены при построении формы. И только в том случае, если полученные значения корректны, произойдёт дальнейшая обработка формы.

Данные из кэша форм стираются в двух случаях:

  • По крону, когда истёк «срок годности» кэша (он кэшируется на 6 часов)
  • После успешного сабмита формы

Если форм и посетителей много, то {cache_form} имеет свойство разрастаться до внушительных размеров. Бороться с этим можно путем двух нехитрых действий:

  • Настроить автоматический запук крона через каждые 1-2 часа.
  • Уменьшить время хранения кэша форм. К сожалению, на данный момент это значение забито жёстко в коде. Поэтому чтобы это исправить надо открыть файл includes/form.inc , найти строку $expire = 21600; (531 строка) и уменьшить её значение, например, до 7200 (2 часа). В этом случае при каждом обновлении ядра вам придётся каждый раз исправлять значения этой переменной (по крайней мере до тех пор, пока это значение не будет вынесено в настройки). Я изменение кода ядра не особо приветствую, но в критических ситуациях можно этим методом воспользоваться.

По опыту могу сказать, что хоть таблица и разрастается, но своевременный запуск крона позволяет держать её размер в более-менее адекватных рамках. Например, на сайте с 15к уников в день и относительно небольшим количеством форм она держится в рамках 200мб (без изменения срока годности кэша форм).

Кэш изображений

Хранится в сегменте {cache_image}. На самом деле пользы в этой таблице никакой нет, т.к. никакой кэш изображений туда не складывается. Таблица зарезервирована модулем Image и может использоваться как хранение сведений о проведении различных манипуляций над изображениями.

Кэш меню

Сегмент {cache_menu} включается при включении модуля Menu и является хранилищем ссылок из всех меню, созданными через интерфейс Друпала. Cache ID строится по правилу links:имя_меню:tree-data:язык:хэш_параметров:

$tree_cid = 'links:' . $menu_name . ':tree-data:' . $GLOBALS['language']->language . ':' . hash('sha256', serialize($parameters));

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

Кроме деревьев здесь также находится и постраничное меню – т.е. меню, которое должно отображаться на конкретной странице.

Кстати, модуль Admin menu тоже хранит здесь свои данные. Правда, он записывает их уже отрендеренными (в виде html кода) и занимает всего одну запись в таблице.

Кэш страниц

Это один из наиболее важных видов кэша. Сегмент с этим кэшем называется {cache_page} и в ней Друпал хранит закэшированные данные страниц для АНОНИМНЫХ пользователей.

В процессе бутстрапа (инициализации системы) Друпал проверяет пользователя и настройки кэширования системы. Если текущий пользователь – аноним, и включена опция кэширования, то Друпал попытается из сегмента {cache_page} вытащить данные, которые он уже показывал другим пользователям, и построить страницу используя полученные данные. В этом случае время загрузки страницы существенно уменьшается, т.к. Друпалу не надо производить построение данных для отображения страницы и рендерить это – ведь эти данные уже лежат в кэше!

Если Друпал нашёл для текущей страницы её кэш, то будут вызваны только 2 хука:

Остальные же хуки (включая hook_init() и прочие) будут пропущены.

Важно понимать, что это именно тот кэш, который вы включаете на сайте в разделе настроек производительности (admin/config/development/performance) с нажатием галочки на «Кэшировать страницы для анонимных пользователей». Если в этом разделе вы выставите настройку «Минимальное время хранения кэша» например в 10 минут, то произойдёт следующее: когда на страницу сайта зайдёт аноним, то страница соберётся для него, а потом закешируется в {cache_page}. Если в течение следующих 10 минут на эту же страницу будут заходить другие анонимные посетители, то им будет показываться та же страница, что и первому анониму. Это несмотря на то, что данные на сайте уже могли обновиться, и авторизованные пользователи уже видят страницу с последними обновлениями. Именно поэтому очень важно настраивать кэширование с полным пониманием того, кому и как должен быть показан контент – кому обязательно его показывать сразу же после создания, а кто может и потерпеть какое-то время ради увеличения производительности.

Из написанного выше вытекает, что для авторизованных пользователей как такового глобального кэша нет. Кэшируются блоки и прочие мелочи технической жизни – и всё. Остальное кэширование лежит на сборщиках / разработчиках сайта. Например, кэширование выводимых данных с помощью Views, или же Panels, или другими модулями. И этот момент обязательно надо учитывать при построении сайта.

Кэш путей

Для более быстрого поиска алиаса по системному пути (и наоборот – системного пути по алиасу) создан отдельный сегмент {cache_path}. Он в себе хранит соответствие между системным путём и его алиасами. Как разработчик я не сталкивался с необходимостью работать с этим кэшем, поэтому я не считаю его важным для изучения. О нём надо знать, что он существует и используется Друпалом для поиска системных путей и алисов. Пожалуй, больше здесь я ничего не расскажу.

Кэш обновлений

При включении модуля Update manager добавляется сегмент {cache_update}. Он в себе хранит данные по всем релизам для включенных модулей. Этот сегмент опять же является не совсем кэшем (на производительность сайта не влияет) – скорее, просто хранилище данных по модулям. Однако обойтись без этого сегмента нельзя – получение данных по релизам модулей занимает достаточно много времени, и без кэширования этих данных некоторые страницы админки, связанные с модулями, загружались бы несколько дольше.

Кэш данного сегмента обновляется только тогда, когда Друпал получает новые данные по релизам модулей. Из этого следует, что никакие сбросы кэша этот сегмент не затрагивают.

Объектный кэш CTools’a

Ещё один кэш, про который не рассказать просто нельзя, хоть он и не относится к ядру. Ещё когда только выходила седьмая версия Друпала я много слышал о каком-то магическом «объектном кэше ctools’a». Как тогда я понял из прочтённого и услышанного, это некая хитрая система, которая делает мир лучше, и по идее увеличивает производительность сайта. Как выяснилось позже – об этом много кто слышал, но никто не интересовался что это и как это работает. А оказалось всё намного банальнее и проще, чем это раздули слухи и больное воображение.

Объектный кэш CTools’a – это сегмент кэша, который предоставляет своё пространство под хранение больших объектов, которые редактируются в данный момент. Приведу пример из модуля Views: при создании или редактировании нового вьюса, до его сохранения, все изменения видны только для того пользователя, который их внёс. Даже если вы измените вьюс, потом закроете сайт, а затем снова откроете – вам будет показана изменённый вами вьюс. Это значит, что объект с изменённым и не сохранённым вьюсом где-то хранился только для вас, в то время как сохранённая вьюха лежала без изменений. Вот теперь вместо этого «где-то» - сегмент объектного кэша CTools’a.

В отличии от других сегментов кэша он имеет дополнительное поле sid (Session ID) – идентификатор текущей сессии пользователя. Именно благодаря нему изменённые данные видны «персонально», то есть только изменившему объект пользователю.

Кстати, этот сегмент не имеет поля expire, и, соответственно, не удаляется при очистке кэша через интерфейс Друпала. Однако раз в сутки CTools запускает крон и удаляет из этого сегмента кэш недельной давности.

P.S.Если я что-то пропустил - дополнения только приветствуются!

Комментарии

Аватар пользователя xandeadx
xandeadx написал:

> даже если блок для какой-то страницы скрыт, Друпал всё равно его загрузит

не правда. друпал одним запросом получает список всех блоков (ф-я _block_load_blocks), а в block_block_list_alter() проверяет видимость. и только если блок видим на странице, будет выполнен hook_block_view()

05.09.2012 03:55
Аватар пользователя Spleshka
Spleshka написал:

Согласен, я упустил из вида имплементацию hook_block_info_alter() в самом блоке. Подправил статью.

05.09.2012 10:37
Аватар пользователя darkdim
darkdim написал:

Зачьот!
Вот бы кто еще разжевал все настройки boost с конкретными примерами;)

05.09.2012 10:55
Аватар пользователя Spleshka
Spleshka написал:

Планирую рассказать про вынос кэша в memcached и boost, в дальнейших статьях про систему кэширования.

05.09.2012 16:58
Аватар пользователя kalabro
kalabro написала:

> Кстати, этот сегмент не имеет поля expire, и, соответственно, не удаляется при очистке кэша через интерфейс Друпала.

Но он удаляется через 7 дней. Код и исус.

05.09.2012 11:01
Аватар пользователя Spleshka
Spleshka написал:

Спасибо за дельное дополнение, добавил в статью.

05.09.2012 17:01
Аватар пользователя kalabro
kalabro написала:

в ctools_object_cache_clean на 7 умножаецо по умолчанию, так что всё в порядке :)

05.09.2012 17:12
Аватар пользователя emzzy
emzzy написал:

Применение механизма кеша на практике от lullabot. В частности упоминается про drupal_static

07.09.2012 12:27
Аватар пользователя Spleshka
Spleshka написал:

Здесь есть часть моей следующей статьи про систему кэширования :) Единственный момент - статические переменные не очень вписывается в систему кэширования Друпала. Это просто одна из особенностей PHP, которая позволяет хранить данные в периоде загрузки одной страницы.

07.09.2012 20:09
Аватар пользователя kalabro
kalabro написала:

Очень ждем продолжения темы по производительности!

15.10.2012 12:12
Аватар пользователя vasiliy0s
vasiliy0s написал:

Да, про memcached очень будет интересно почитать!

10.03.2013 00:45
Аватар пользователя winch
winch написал:

Подскажите пожалуйста:
Мне надо, чтобы все страницы для анонимных пользователей были закешированны, но при этом один из выводимых блоков никогда не для кого не кешероался.
Установка блоку параметра DRUPAL_NO_CACHE ничего не дала — я так понимаю, из-за того что кешируется сами страницы, содержащие этот блок.
как же быть? это вообще возможно сделать?

29.11.2013 18:57
Аватар пользователя AmiGator
AmiGator написал:

«кэширование для блоков отключается, если включаются модули по работе с доступами к материалу, а конкретнее - имплементация hook_node_access»
а как узнать какой модуль задействует hook_node_access? просто поиск по модулям выдает кучу их, но блокирует кеш, как мне кажется один

19.11.2014 13:12

Комментировать