Drupal 8, Cache & Rock N' Roll

Вс, 07/02/2016 - 19:19

Вообще, мне бы самому хотелось послушать про кэширование в 8-ке. Но что-то не особо говорят. Вот и решил подсобрать инфы, чтобы подогреть интерес. Сразу скажу, что если не пишешь собственный модуль, то всё это не так и нужно. Но не зря же я втыкал в Word (вместо видосов с котами), так что все читайте, уж будьте бобры :)

Ну и чо там?

Из Symfony в Drupal 8 привита всеобщая кэшируемость. Теперь для любого объекта, который участвует в рендеринге, можно задать свои параметры кэширования.

Выглядит так:

'#cache' => [
    'keys' => ['entity_view', 'node', 5, 'teaser'],
    'contexts' => ['languages', 'timezone'],
    'tags' => ["node:5", 'user:3'],
    'max-age' => Cache::PERMANENT,
  ],

Всего 4 параметра (указывать все необязательно):

keys – массив значений, из которых будет сформировано название для кэша (ID). Типичный набора:
'keys' => array( 'entity_view',  $this->entityTypeId,  $entity->id(),  $view_mode)

contexts –зависимость от эм.. контекста, например языка и временной зоны. Для тех, кто сечет в HTTP заголовках, в качестве его аналогии приводят Vary. Еще примеры возможных значений: cookies, route, session, url, user и т.д. Можно задавать более конкретную привязку, например user.permissions – зависимость только от прав пользователя, или url.query_args:foo – зависимость от параметра foo в адресной строке запроса.При их обработке происходит оптимизация. Например:
[user, user.permissions] => [user], т.к. зависимость от user уже включает в себя user.permissions.

tags – зависимость от конкретных объектов, например от 5-ой ноды, или пользователя с id = 3. Интересно, что можно задавать значения типа config:block_list (зависимость от конфигурации блоков), или config:filter.format.basic_html. А еще node_list.
В значениях тега не должно быть пробелов, т.к. пробел используются в качестве разделителя при передаче через header (странно, конечно, что выбран пробел, ну да не о том речь). Этот самый заголовок (X-Drupal-Cache-Tags) интересен для настройки супербыстрой раздачи через систему обратного проксирования (Varnish, CDN).

max-age – время хранения кэша в секундах (0 – не хранить, Cache::PERMANENT (-1) – без ограничения)

Не надо бла-бла, давай пример

Задача: разработать блок, который будет выводить значение параметра xxx переданного через адресную строку.

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


<?php

/**
 * @file
 * Contains \Drupal\cacheable_block\Plugin\Block\CacheableBlock.
 */

namespace Drupal\cacheable_block\Plugin\Block;

use Drupal;
use 
Drupal\Core\Block\BlockBase;
use 
Drupal\Core\Annotation\Translation;
use 
Drupal\Core\Routing;

/**
 * Provides a 'Cacheable Block' block.
 *
 * @Block(
 *   id = "cacheable_block",
 *   admin_label = @Translation("Cacheable block"),
 * )
 */
class CacheableBlock extends BlockBase {

  /**
   * {@inheritdoc}
   */
  
public function build() {

    $key 'xxx';
    
$message "";
    
$value Drupal::request()->query->get($key);

    if($value) {
      
$time date("Y-m-d H:i:s");
      
$vars = array('@value' => $value'@time' => $time);
      
$message $this->t('@value (@time)'$vars);
    }

    return array(
      '#markup' => $message,
      
'#cache' => [
        
'contexts' => ["url.query_args:$key"],
      ],
    );
  }
}
?>

Пример работы:

my.site?xxx=Kate                   -> Kate  (2016-02-07 11:11:11)
my.site?xxx=Ann                    -> Ann  (2016-02-07 22:22:22)
my.site/node/666?xxx=Ann           -> Ann  (2016-02-07 22:22:22)
my.site?xxx=Kate                   -> Kate  (2016-02-07 11:11:11)

Т.е. если xxx повторяется – значение берется из кэша, независимо от времени и страницы, и наоборот, стоит xxx измениться, и блок выдает другое значение (которое тоже кэширует).

Правда если зайти под другим пользователем, то будет другой кэш, хоть никакой зависимости от пользователей и не указано. И так и не понял, как задать
'cache' => DRUPAL_CACHE_GLOBAL

Но, можно делать свой кэш с нужным значением и ключами:

<?php
$value 
Drupal::request()->query->get($key);
$message Drupal::cache()->get("cacheable_block-$key-$value")->data;
if(
$value && !$message) {
  
$time date("Y-m-d H:i:s");
  
$vars = array('@value' => $value'@time' => $time);
  
$message $this->t('@value (@time)'$vars);
}
Drupal::cache()->set("cacheable_block-$key-$value"$message);
?>

Помни! Если кто-то вобьёт мильярд разных вариантов xxx – ничем хорошим это не закончится. Прямая зависимость кэша от столь легко меняющейся (и бесконечновариантной) зависимости – крайне не рекомендуется.

Еще пример

В блоге Acquia Dev есть пример, в котором рассматривается конвертация изображения в asci-символы. Вот краткая выжимка:

Как тебе такое решение?

<?php
$build 
= array(
  
'#theme' => 'ascii_art',   
  
'#attributes' => array('class' => 'ascii-art'),   
  
'#caption' => 'My favorite animal',   
  
'content' => array(
    
'#markup' => generate_ascii_art('llama.png')
  ), 
);
?>

Здесь клёвая картинка ламы будет преобразована в ASCII символы. И, будь уверен, преобразование просто прекрасно! Но сейчас разговор не об искусстве ASCII. Динамическая генерация данных на основе изображения, разве это не медленно, спросишь ты. Ясен день! В конце-то концов, как следует преобразовать пушистую ламу в набор символов не так-то и просто. Нет, без шуток, можно придумать кучу случаев затратного рендеринга (требующего сложных вычислений и кучи запросов к бд). Так что кэширование результатов здорово бы помогло. И вот как это можно устроить:

<?php
$build 
= array(
  
'#theme' => 'ascii_art',
  
'#attributes' => array('class' => 'ascii-art'),
  
'#caption' => 'My favorite animal',
  
'#cache' => array(
    
'keys' => array('ascii-art''llama'),
  ),
  
'#pre_render' => array('ascii_art_pre_render'),
  
'#ascii_art_image' => 'llama.png',
);

function ascii_art_pre_render($build) {   
  
$build['content'] = generate_ascii_art($build['#ascii_art_image']);   
  return 
$build
}
?>

Что за магические значения?

Завязывать кэш на конкретные значения (5-ую ноду, 3-го пользователя) – это, конечно, не солидно. А главное, вряд ли нужные значения известны. Поэтому в реальности, конечно, всё описывается программно.

Например, нужно связать кэш со списком нод из какой-то выборки

<?php
$nids 
= [];
foreach (
$result as $row) {
    
$nids[] = $row->nid;
}
$cache_tags  Cache::buildTags('node'$nids));
'#cache' => array(
    
'keys' => $cache_tags,
),
?>

Яма, яма, яма, ямллл…

Кстати, простые настройки можно указывать и через YML

renderer.config:
    auto_placeholder_conditions:
      max-age: 0
      contexts: ['session', 'user']
      tags: []

Настройка через API:

$metadata = new CacheableMetadata();
$metadata->setCacheContexts(['qux'])
      ->setCacheTags(['foo:bar'])
      ->setCacheMaxAge(600);

Хочу свой рендеринг с кешем и зависимостями

Вот, пожалуйста:


<?php
$renderer 
= \Drupal::service('renderer');

$config = \Drupal::config('system.site');
$current_user = \Drupal::currentUser();

$build = [
  
'#prefix' => '<aside>',
  
'#markup' => t('Hi, %name, welcome back to @site!', [
    
'%name' => $current_user->getUsername(),
    
'@site' => $config->get('name'),
  ]),
  
'#suffix' => '</aside>',
  
'#cache' => [
    
'contexts' => [
      
// The "current user" is used above, which depends on the request,
      // so we tell Drupal to vary by the 'user' cache context.
      
'user',
    ],
  ],
];

// Merges the cache contexts, cache tags and max-age of the config object
// and user entity that the render array depend on.
$renderer->addCacheableDependency($build$config);
$renderer->addCacheableDependency($build, \Drupal\user\Entity\User::load($current_user->id()));
?>

Какие есть еще API?

Теги и контексты – обычные массивы, и ничто не мешает их склеить обычным сложением
$tags = $account->getCacheTags() + $comment->getCacheTags()
Но престижней использовать специально обученные метод, ведь это и инкапсуляция и избавления от повторок
Cache::mergeTags($main_tags, $add_tags);
Все это верно и для contexts, и для max-age.
Если в запросе нет подходящего параметра, чтобы подставить в keys, можно использовать его hash (sha256), через метод keyFromQuery.
Еще есть validateTags, invalidateTags и getBins (bins – контейнеры, в которых хранятся кэши, обычно по названию модуля, к которому принадлежат их значения)

Entity и Config – готовы, а ты?

У всех entity или configuration уже настроены параметры кэширования. Получить их можно так:

\Drupal::entityManager()->getDefinition('node')->getListCacheTags();
Или через конкретные объекты:
$node->getEntityType()->getListCacheTags()
(вместо $node, может быть $user, $view…)

Аналогично для contexts и max-age
$this->entityType->getListCacheContexts()

Что там есть в Examples/cache_example

Хоть там todo больше, чем кода, но можно глянуть как вручную сохранять/получать/удалять кэш:

<?php
cacheBackend
->set('название_кэша''содержимое_кэша'параметры_кэширования);
$cache cacheBackend->get('название_кэша'); // и потом $cache->data
cacheBackend->delete('название_кэша');
?>

Где cacheBackend - это объект реализующий интерфейс CacheBackendInterface.
Там же, в качестве примера, подсчитывают количество файлов в ядре, угадаешь сколько? А вот и нет, всего лишь 5928.

Так кому, что и почему надо знать?

При описании «революционных изменений» Drupal 8, часто упоминают «кэширование по умолчанию». Но это не просто кэширование. Два модуля, Internal Page Cache и Dynamic Page Cache затащили это дело на новый уровень. Вот как описывает историю их создания ведущий разработчик Вим Лирс (вкратце):

Как-то Дрис начал парить мне голову вопросами о ESI. Я сказал, что то, что у нас называется "поддержкой ESI" - бред, но есть реальный шанс сделать хорошо. Дриса это так проняло, что он даже выложил пост :) А мне пришлось изрядно напрячь башку, чтобы реализовать то, что посулил в запале.

Сначала наша команда довела до ума кэш тэги, благодаря чему Page Cache (ныне Internal Page Cache) стал работать так хорошо, что мы включили его в сборку и включили по умолчанию. А затем добили и контексты кэша. Это далось не просто, пробовали и так и сяк. В резульате на пару с Фабьеном запили через пузырёк. Потом еще 7 месяцев просыхали фиксили, но это того стоило. И теперь Dynamic Page Cache (бывший Smart Cache) тоже в сборке, и тоже по умолчанию.

Конечно, еще есть над чем работать, напрягает и время перед запуском и всякая другая лабуда, но у нас еще есть идеи на этот счет. Кстати, как вам BigPipe?

И еще раз

То, что эти модули включены по умолчанию, означает, что каким бы ты ни был лохом, кэширование на сайте Drupal 8 будет огонь! Только не выключай их. Хотя, если для анонимных пользователей нужны разные результаты (например, с учетом их сессии), то Internal Page Cache придется отключить (либо мутить через AJAX), но это уже другая история.

А в этой истории речь о том, что теперь больше не нужны никакие танцы с Authcache, где нужно учитывать весь код сайта. Более того, опять пошли разговоры, что владелец сайта (админ, маускликер, сайт-билдер) может забыть, что значить фраза «очистить кэш». Поскольку теперь разработчики модулей могут и должны сами настроить зависимости кэша. Контриб-модули даже обязаны предоставить тесты на годность в режиме кэширования.

Но если Dynamic Page Cache мешает запалу разработки, можно временно его отключить, для этого в папке sites/название_сайта/ создается settings.local.php с кодом:

$settings['cache']['bins']['render'] = 'cache.backend.null';

Когда это будет в 7-ке?

Никогда. В Drupal 7 не появится мгновенного обновления кэша (в Drupal 8 кэш обновляется сразу по факту устаревания, не дожидаясь запроса, ога). Не появится Dynamic Cache Page. И не будет BigPipe. Как-то так.

А где же та фраза?

«There are only two hard things in Computer Science: cache invalidation and naming things. -- Phil Karlton»

Склад ссылок, которых здесь еще не было (хотя быть должны)

https://www.drupal.org/developing/api/8/cache
https://www.drupal.org/developing/api/8/render/pipeline
https://www.drupal.org/developing/api/8/render/arrays/cacheability/auto-placeholdering
http://wimleers.com/talk/making-drupal-fly-fastest-drupal-ever-here
http://wimleers.com/blog/drupal-8-page-caching-enabled-by-default
https://drupalwatchdog.com/volume-4/issue-1/automagic-speed-cache
http://tech.dichtlog.nl/php/2015/08/03/lazy-builder-callback.html
http://www.sitepoint.com/exploring-cache-api-drupal-8/
https://github.com/upchuk/cache_demo_d8
http://slides.com/mikkeschiren-1/d8-cache#/1
http://chimera.labs.oreilly.com/books/1230000000845/ch01.html#_queues_and_workers

9 Спасибо

Комментарии

Аватар пользователя gor
5 months 3 недели назад gor #

Спасибо! Ушло на главную.

2 Спасибо
Аватар пользователя Director-cemetery
5 months 3 недели назад Director-cemetery #

Спасибо за статью, мои отношения всегда были дружественными с 8-кой. А теперь я её полюбил ещё больше. Семёрка мне давалась значительно сложнее. Теперь можно тестировать сайт на любом бесплатном сервере, практически на минимальных ресурсах.
Что самое важное - знания программиста теперь не важны. Это именно Система Управления Контентом.

0 Спасибо
Аватар пользователя Valeratal
5 months 3 недели назад Valeratal #

Модуль Boost кладет все это модное хипстерское восьмерочное кэширование на лопатки

2 Спасибо
Аватар пользователя vaplas
5 months 3 недели назад vaplas #

Спасибо за отзывы! На что не пойдешь, после появления кнопки "Спасибо" :)

Valeratal написал:
Модуль Boost кладет все это модное хипстерское восьмерочное кэширование на лопатки

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

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

Просто у меня скомканный обзор получился (воскресенье заканчивалось, а хотелось успеть впихнуть побольше всего). Основной то посыл - это динамичность кэша, т.е. кэш с заглушками, вместо которых будут подставляться нужные данные (в том числе и закэшированные). А BigPipe отличный пример, как этот механизм можно перевести на AJAX рельсы.

0 Спасибо
Аватар пользователя Valeratal
5 months 3 недели назад Valeratal #

« чтобы выдавать кэш, на основе всей логики сайта »

чтобы выдавать кэш на основе всей логики сайта, нам понадобится

1. Уйма вариантов этого кэша
2. Уйма ресурсов чтобы кэшить, потом читать ис кэша и тд итп

«Да и кэшировать многотонные страницы, разница между которыми в паре байт - не такой уж айс.»

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

0 Спасибо
Аватар пользователя Valeratal
5 months 3 недели назад Valeratal #

А так да, чудес не бывает. Чем более изощренный кэш, тем больше нужно ресурсов

0 Спасибо
Аватар пользователя vaplas
5 months 3 недели назад vaplas #
Valeratal написал:
«Да и кэшировать многотонные страницы, разница между которыми в паре байт - не такой уж айс.»
вот это я не понял. Кстати, страницы бустом кэшируются как хтмл-код. то есть ничего многотонного там нет

Многотонными являются затраты на генерацию данных в этом хтмл-коде. Куча вычислений, запросов к базе и другим сайтам (открытому апи, конечно). А все из-за того, что в одном из блоков выводится "шутка дня (часа, минуты)" или надпись:

username, ты лучш{ий|ая}!

А пользователь вдруг решил поменять своё имя или пол (такие времена).

Можно провести анализ, найти тяжелый рендеринг и закэшировать его, подмешать AJAX. Но кто и почему должен этим заниматься? Отсюда и правда жизни - "подключим буст, память то копеешная", "кэш медленно отдается - ну так где мемкэш". Но это только отмашка от проблемы, потому что никто просто не хочет нянчиться с кэшем. Неблагодарное это занятие.

Но в друпал 8 все не так. Тут кэш - это не сторонний работничек, который дожидается хтмл-результата, а потом кэширует его. Это внутренний демон, который знает о каждом объекте участвующем в рендеринге страницы, знает о каждой его зависимости (благодаря прописанным tags, contexts и max-age), и сам все компонует. Никакой уймы вариантов кэша, никакого натягивания и подтасовки, все динамические части хмтл-ответа вынесены в отдельные кэши (которые по необходимости также разбиты на составные куски), все прозрачно, как в шаблонизаторе.

На бутс и всякое проксирование это не налагает никакого табу, все будет добре короч)

0 Спасибо
Аватар пользователя WebRemake
5 months 3 недели назад WebRemake #

Спасибо земляк! Классная статья - и по форме и по содержанию.

1 Спасибо
Аватар пользователя Valeratal
5 months 2 недели назад Valeratal #

Ну вот будут реальные кейсы сравнения, тогда будет что обсуждать

Пока... 7-ка рулит по всем тестам

0 Спасибо
Аватар пользователя Valeratal
5 months 2 недели назад Valeratal #

Сейчас друпал8 и кэширование - это "хотели как лучше, получилось как всегда" :)

0 Спасибо
Аватар пользователя Director-cemetery
5 months 2 недели назад Director-cemetery #
Valeratal написал:
Сейчас друпал8 и кэширование

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

0 Спасибо
Аватар пользователя gorr
5 months 2 недели назад gorr #

Отличная статья, спасибо!

0 Спасибо
Аватар пользователя vaplas
5 months 2 недели назад vaplas #

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

Например, на странице выводится статистика (требующая 10 с. на сбор) и какая-нибудь мелкая (но постоянно меняющаяся) инфа. 8-ка возьмет статистику из кэша за 0.01 с, а в 7-ке придется ей получать заново (если, конечно, вообще среагирует на меняющуюся инфу). В результате друпал 8 в 1000 раз быстрее? Или так неправильно мерить, потому что в 7-ке не заложен такой механизм? Но об том и речь!

Можно повозиться и настроить 7-к, но тогда статистика не учитывает затрат на настройку (которую нужно делать с учетом всего, что влияет на страницу, в то время как в 8-ке каждый модуль сам берёт на себя свои проблемы).

А то, что Drupal 7 (выступающий на арене более 5 лет) по практичней 3-месячного Друпал 8 у которого молоко еще на губах не обсохло - спору нет. Просто когда это молоко обсохнет, не нужно говорить, что никто не предупреждал :)

0 Спасибо
Аватар пользователя Valeratal
5 months 2 недели назад Valeratal #

Кейсы, ну возьмем например HR-Portal, достаточно посещамый сайт

Там стоит буст сейчас (мин. кэширования - 1 сутки, максимум - 2 суток). Вопрос, если мы воткнем туда 8-ку с ее хваленым кэширование, что будет?

Вот такой эксперимент и надо проводить.

А то нагенерируют страниц, и заходят 1-им (одним) юзером без всякой нагрузки

позиция непринятия

Это не позиция, это следствие текущей реальности, в друпале и на этом сайте

0 Спасибо
Аватар пользователя Old Human
2 months 5 дней назад Old Human #

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

function hook_block_view_my_block_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block) {
$build['#title'] = t('New title of the block');
$build['#cache']['max-age'] = 0;
}

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

0 Спасибо
Аватар пользователя vaplas
2 months 5 дней назад vaplas #

Альтернативный метод: в файл НАЗВАНИЕ_ТЕМЫ.theme добавить hook_preprocess_HOOK() (если такой уже есть, то вставить код туда) и почистить кеш. Например, если тема называется my_bartick, а дескрипшен блока "My love block", то будет так:

<?php
/**
 * Implements hook_preprocess_HOOK() for block.html.twig.
 */
function my_bartik_preprocess_block(&$variables) {
  
  if (
$variables['attributes']['id'] == 'block-myloveblock') { // block-названиебезпробелов
    
$variables['#cache']['max-age'] = 0;
  }

 # другой код
}
?>

1 Спасибо
Аватар пользователя Old Human
2 months 5 дней назад Old Human #
vaplas написал:
Альтернативный метод:

Целую ваши мысли )
Все работает, спасибо!

0 Спасибо