Drupal 8 + Varnish: Кешируем HTML правильно

  • Tutorial
Drupal 8 – это самый свежий релиз CMS Drupal. Varnish – это HTTP reverse proxy cache, надстройка над вебприложением, которая позволяет кешировать HTTP ответы в ОЗУ сервера.

Когда мы ставим Varnish перед Drupal’ом (либо любым другим вебприложением), схема обработки входящего HTTP запроса трансформируется следующим образом.

image

Еще во времена Drupal 6 и 7 с помощью Varnish’а было очень удобно кешировать статические ресурсы (рисунки, CSS, JavaScript файлы). Но были пробемы с кешированием HTML страниц – не существовало удобного механизма выборочной инвалидации кеша. Оставалось только либо сознательно отдавать устаревший кеш, либо полностью очищать кеш в Varnish при каких-либо изменениях в Drupal. Оба подхода имели свои недостатки.

Если при каком-либо изменении в Drupal’е вы не инвалидировали кеш Varnish’а, то нужно было ставить относительно маленький TTL, чтобы кеш Varnish’а достаточно быстро устаревал и Varnish обновлял свой кеш новыми данными из Drupal’а.

C другой стороны, если вы на “каждый чих” в Drupal, очищали кеш Varnish’а, то зачастую эти меры оказывались избыточными. Какой-нибудь юзер мог написать безобидный комментарий под малопосещаемой страницей, и это приводило к инвалидации всего кеша Varnish’а (в том числе и очень посещаемых страниц, которые на самом деле не поменялись из-за добавленого комментария).

Drupal 8 Cache API


К счастью, дела обстоят куда лучше в Drupal 8, где была добавлена система гибкого кеширования через мета-информацию. Я настоятельно рекомендую любопытным почитать официальную документацию. Вкратце, эта система позволяет Drupal’у отслеживать от чего (ноды, комментарии, конфиги и т.д.) зависит сгенерированная страница. Каждая страница ассоциируется с набором кеш тегов (cache-tags). Затем, при любом изменении состояния, Drupal распознает какие кеш теги необходимо инвалидировать. Это в свою очередь позволяет Drupal’у инвалидировать лишь минимально необходимый набор закешированых страниц, когда что-то (нода, комментарий, конфиг) изменилось. Именно такой подход используется в ядре Drupal’а для инвалидирования внутреннего кеша. С помощью пары ловких движений можно добиться схожего “минимально необходимого” инвалидирования в кеше Varnish’a.

Расширяем Cache API на Varnish


Этот туториал считает, что вы уже успешно установили и настроили Varnish для статических ресурсов (графика, css, javascript). Для покрытия кешем еще и анонимных HTML страниц нам потребуется 3 шага:
  1. Объяснить Varnish’у какая страница зависит от каких кеш тегов. Drupal уже имеет это знание, его просто нужно передать Varnish’у.
  2. В момент, когда какой-то кеш тег инвалидируется в Drupal’е, нужно также инвалидировать все страницы в кеше Varnish’a, которые зависят от этого кеш тега.
  3. Подготовить Varnish ко всем этим нововведениям.

Все эти вещи уже реализованы в виде модулей в экосистеме Drupal’а. Нам потребуется:
  • Purge – своего рода фреймворк для инвалидации внешних HTTP кешей (Varnish, CDN, Nginx)
  • Varnish Purger – реализация этого фреймворка под Varnish.

Если вы используете composer, то:
composer require "drupal/purge:^3.0"
composer require "drupal/varnish_purge:^1.0"

Включите следующие модули:
  • Purge (purge)
  • Purge Drush (purge_drush)
  • Purge Tokens (purge_tokens)
  • Purge UI (purge_ui)
  • Cron processor (purge_processor_cron) или Late runtime processor (purge_processor_lateruntime). О разнице между этими 2 модулями чуть позже.
  • Core tags queuer (purge_queuer_coretags)
  • Varnish Purger (varnish_purger)
  • Varnish Purger Tags (varnish_purge_tags)


Настройка Drupal 8


Нужно объяснить Drupal’у, где находится Varnish и как частично инвалидировать его кеш. После установки вышеперечисленных модулей, перейдите на страницу /admin/config/development/performance/purge. Добавьте Varnish purger, укажите правильный путь к хосту, где находится Varnish.
В Type укажите Tag, т.к. мы используем кеш теги Drupal’а для инвалидации. В Request method укажите Ban, т.к. соответствующая часть конфигов Varnish’а ожидает именно такой метод HTTP запроса.
image

На вкладке Headers:
image

Добавьте заголовок Cache-Tags c содержимым [invalidation:expression]. Это токенизированное выражение будет заменено на значение инвалидированного кеш тега модулем Token.

Остальное можно (и нужно) конфигурировать под ваш вкус и конкретную ситуацию. Теперь Drupal будет включать заголовок Cache-Tags во все свои ответы, куда будет вписывать список кеш-тегов, участвовавших в формировании сгенерированной страницы. Varnish будет хранить эти заголовки, как часть своего кеша и будет их использовать для инвалидации кеша.

В этом месте удостоверьтесь, что ваш Drupal при запросе напрямую (минуя Varnish) действительно содержит Cache-Tags заголовок. В моем случае он выглядит вот так для первой попавшейся страницы:
спойлер
Cache-Tags: block_view config:block.block.seven_breadcrumbs config:block.block.seven_content config:block.block.seven_help config:block.block.seven_local_actions config:block.block.seven_login config:block.block.seven_messages config:block.block.seven_page_title config:block.block.seven_primary_local_tasks config:block.block.seven_secondary_local_tasks config:block_list config:coffee.configuration config:shortcut.set.default config:system.menu.admin config:user.role.administrator config:user.role.authenticated http_response rendered user:1

По умолчанию, Drupal отвечает Cache-Control: private, no-cache (т.е. запрещает кеширование) на все HTML ресурсы, которые он генерирует. Это значит, что Varnish не будет их кешировать. Нужно включить внешнее кеширование в настройках Drupal’а. Перейдите на страницу /admin/config/development/performance и выберите ненулевое значение для Page cache maximum age.
image

Я рекомендую начать с какого-нибудь безобидного 5-минутного кеша, и только когда все отлажено и успешно обкатано, переходить на полномасштабные значения длиной в часы либо дни.

Опять же, перепроверьтесь – запрашивая страницу Drupal’a анонимным пользователем, вы должны увидеть в ответ: Cache-Control: public, max-age=[ненулевое время жизни кеша]. А при запросе от имени авторизированного пользователя, в ответ должен прилететь Cache-Control: private, no-cache. Таким образом мы разрешили Varnish’у кешировать анонимные и только анонимные HTML страницы.

Настройка Varnish


Теперь перейдем к подготовке Varnish’a. В ваш VCL необходимо добавить следующее.

Во-первых, Drupal теперь будет слать HTTP Ban запросы на ваш Varnish. Прежде всего мы должны ограничить доступ к обработке этого запроса, иначе кто угодно сможет чистить кеш Varnish’a. Для этого введем acl группу:
# Список адресов, с которых разрешен Ban запрос.
acl purge {
    "127.0.0.1";
}

Нужно еще добавить саму обработку Ban запросов. В ваш vcl_recv впишите в самое начало:
if (req.method == "BAN") {
  # Проверим права на запрос.
  if (!client.ip ~ purge) {
    return (synth(403, "Not allowed."));
  }

  # Очистим кеш по “Cache-Tags” заголовку.
  if (req.http.Cache-Tags) {
    ban("obj.http.Cache-Tags ~ " + req.http.Cache-Tags);
  }
  else {
    return (synth(403, "Cache-Tags header missing."));
  }
        
  # Уведомим об успешной обработке запроса.
  return (synth(200, "Ban added."));
}

Так же, по умолчанию, Varnish будет ре-транслировать заголовки Cache-Tags и Cache-Control, которые он получит от Drupal’a, в свой ответ конечному клиенту (браузеру).

В случае Cache-Tags – это банально мусор, который вы будете гонять по сети и лишний источник информации о внутреннем устройстве вашей инфраструктуры. А в случае Cache-Control: public – это довольно нежелательное поведение, т.к. Drupal разрешил кеширование для Varnish’a, но кешировать страницы в браузерах ваших посетителей было бы некорректно (ведь они тогда с опозданием получат обновленный контент и вся наша умная схема про инвалидирование кешей пойдет на смарку). Посему в vcl_deliver надо добавить:
# Убираем ненужный для клиента заголовок.
unset resp.http.Cache-Tags;
# Запрещаем кеширование на клиенте для динамических ресурсов.
if (resp.http.Content-Type ~ "text/html") {
  set resp.http.Cache-Control = "private,no-cache";
}

На этом этапе можно перекреститься и перезапустить Varnish на живом сервере. Теперь Varnish умеет выборочно чистить свой кеш по HTTP Ban запросам, которые ему будет слать Drupal. Так же он будет умным образом кешировать HTML ресурсы (те, которые Drupal ему разрешит), но не будет позволять бразуерам их кешировать. Именно отсюда и происходит его название, как класс – reverse HTTP proxy cache – кешируем на HTTP прокси, а не на клиенте.

Плюшки, фантики и печеньки


Перепроверьте правильность всей конфигурации. Узнать об этом будет куда больнее от клиента в 2 часа ночи, чем сразу после внесения изменений. Поверьте моему опыту интуиции.

Модули Cron processor и Late runtime processor


Я упомянул о том, что нам понадобится один из этих 2 модулей. Инвалидация кеша Varnish’а сделана по принципу очереди – в процессе обработки запроса Drupal вставляет в очередь определенные кеш теги, и потом некто должен обработать эту очередь, отправив соответствующие HTTP Ban запросы на Varnish. Оба модуля решают задачу обработки очереди, но делают это по-разному.

Cron processor обрабатывает очередь, когда запускается cron Drupal’а. Это значит, что cron становится стратегически важным (он и раньше должен таковым считаться). Также, из этой модели следует, что существует задержка вплоть до Х (где Х – это частота запуска cron’а), когда Varnish может отдавать закешированый и уже устаревший HTML контент. В некоторых случаях это приемлемо.

Альтернативный (и соответственно более агрессивный) обработчик очереди – это Late runtime processor. Он обрабатывает очередь кеш тегов в конце обработки каждого входящего HTTP запроса, т.е. в реальном времени.

Выбирайте один из двух обработчиков на ваш вкус и требования, представляемые к вебсайту. У меня сложилось впечатление, что большинство Drupal 8 вебсайтов используют Cron processor. Однако, проблем и откровенных багов с Late runtime processor также не зарегистрировано.

Канал коммуникации Drupal → Varnish


С хоста, где находится Drupal, сделайте curl -X BAN --header 'Cache-Tags: dummy-cache-tag' http://varnish-server.com
Удостоверьтесь, что Varnish вам ответил 200 HTTP OK. Это значит, что вы правильно настроили Varnish, и он готов обрабатывать запросы на инвалидацию, которые будет слать Drupal.

Дебаггинг информация в Drupal watchdog


На странице /admin/config/development/performance/purge вы можете настроить необходимый уровень логирования в watchdog Drupal’a. Очень полезно, когда нужно идентифицировать проблему.

Размер очереди на инвалидацию


На странице /admin/config/development/performance/purge присутствует некоторый текущий статус. В частности, обратите внимание на счетчик Queue size. Он показывает текущую длину очереди на инвалидацию кеш тегов – это количество кеш тегов, которые уже инвалидировались в Drupal’e, но еще не были инвалидированы в Varnish’e.

Если вы используете инвалидацию через cron, убедитесь, что это число падает до нуля после запуска cron’a.

Если же у вас используется “Late runtime processor”, то вам следует ожидать ненулевые значения лишь периодически. Слишком высокая цифра, либо длительно не спадающая до 0 будет признаком каких-то проблем.

Cache hit в Varnish’е


Не нужно верить теории. После проделанной работы, лучше перепроверить на практике факт, что Varnish правильно кеширует анонимные HTML страницы! Если ваш Varnish изначально не вставляет заголовки о cache hit в свои ответы, то достаточно добавить в vcl_deliver:

if (obj.hits > 0) {
  set resp.http.X-Varnish-Cache = "HIT";
}
else {
  set resp.http.X-Varnish-Cache = "MISS";
}

Удостоверьтесь на практике, что запрашивая страницу от анонимного пользователя, Varnish ее закешировал (со второго раза он должен отвечать заголовком X-Varnish-Cache: HIT). В этот момент спровоцируйте инвалидацию исследуемой страницы в Drupal’е – почистите кеш Drupal’a либо пересохраните ноду (если вы анализируете страницу какой-нибудь ноды) и т.п. Не забудьте запустить cron Drupal’a, если он ответственен за инвалидацию кеша в Varnish’e.

Повторите запрос на исследуемую страницу. Первый ответ должен содержать заголовок X-Varnish-Cache: MISS — таким образом вы подтвердите, что Drupal успешно уведомил Varnish об инвалидации необходимых кеш тегов, и последний успешно обработал полученный запрос на инвалидацию.

Длина заголовков HTTP ответа


Сайты на Drupal 8 очень часто могут оказаться громоздкими. В частности это может вылиться в длинный список кеш тегов, ассоциируемых с определенной HTML страницей. Многие вебсервера и вообще Webserver + PHP стек в разных реализациях может иметь ограничение на максимальную длину заголовков ответа. Об это действительно можно споткнуться. Причем, если тестовое окружение имеет тестовый (урезанный) набор данных, то проблема всплывет только на живом окружении. Лечить проблему нужно в зависимости от того, как у вас развернут стек.

Update: Оказалось, что там не все так просто с длиной заголовков ответа. Вот issue на drupal.org, где я загрузил патч. Пачт весьма неплохо справляется с задачей уменьшения длины заголовков www.drupal.org/project/purge/issues/2952277

Еще раз о Cache API в Drupal 8


Описанное в статье — это лишь малая часть того, как используется cache API в Drupal 8. Я лично считаю Cache API главным новведением по сравнению с Drupal 7. Если у читателей есть интерес, то я могу написать следующую статью, где рассмотрю глубже вопросы внутреннего устройства cache API; как кеширование переплетается с рендерингом в Drupal 8.
  • +10
  • 3,2k
  • 7
Поделиться публикацией
Комментарии 7
    +1
    Спасибо!
      +1

      А все-таки, зачем нужен Varnish при наличии кэша в nginx? Не сарказм, правда интересно, в чем разница

        0
        Я nginx как прокси кеш еще ни разу не использовал, поэтому практического опыта не имею. В конкретной ситуации, для которой я разбирался с Drupal 8 + Varnish, меня просто попросили посмотреть со словами «вот, что-то сайт тупит» :) И там уже было HAproxy (ssl termination) -> Varnish (proxy caching) -> Apache -> mod_fcgi (php).

        Я немног покопался сейчас, и похоже, что по производительности они оба очень похожи. Но на сайте Nginx я нашел следующее (https://www.nginx.com/blog/nginx-caching-guide/):

        Does NGINX Support Cache Purging?

        NGINX Plus supports selective purging of cached files. This is useful if a file has been updated on the origin server but is still valid in the NGINX Plus cache (the Cache-Control:max-age is still valid and the timeout set by the inactive parameter to the proxy_cache_path directive has not expired). With the cache‑purge feature of NGINX Plus, this file can easily be deleted. For more details, see Purging Content from the Cache.

        Получается, что как раз главный конек, на котором построена выборочная инвалидация по кеш тегам, отсутствует в Nginx. Не возмусь со 100% уверенностью сказать, что опен-сорсный nginx дейсвтительно никак не может инвалидировать по заголовкам, но похоже на то.
          0

          А в чем проблема кэшировать с etag?
          Страница изменилась, создали новый кэш и выдали в заголовках новый etag. Nginx прекрасно это умеет

            0
            Про etag согласен. Но как тогда проактивно инвалидировать кеш Nginx'a из Друпала? В схеме, как описано в этой статье, когда какая-то нода изменилась, то Друпал отправит запросы на HTTP proxy о том, что такую-то, такую-то и такую-то страницы (страницы, где эта нода в том или ином виде присутствует), нужно выкинуть из кеша.

            Без этой проактивной инвалидации, нам либо сознательно отдавать потенциально устаревший контент либо nginx должен чуть ли не каждый раз перепроверять свой кеш против друпала (сверять etag).

            Я посмотрел на вот эту (https://cgit.drupalcode.org/nginx_cache_clear/tree/nginx_cache_clear.module) реализацию под Nginx + Drupal 7, и там довольно топорный метод — удаление файла кеша из файловой системы.
        +1
        Есть тесты (графики) производительности с varnish и без него?
          0
          Тестов и графиков нет =) Но у меня не вызывает сомнений, что анонимные страницы будут куда эффективнее обрабатываться Varnish'ом, чем Drupal 8 при включенном кеше анонимных страниц на последнем. Даже если этот кеш в Drupal'е положить в оперативку (Redis,Memcached).
          Включение во всю формулу PHP тут же даст свой негативный результат. Concurrency тоже значительно ухудшится, т.к. потребление ОЗУ на 1 обработанный запрос будет значительно выше. У Varnish'а накладные расходы по памяти на обработку 1 запроса минимальные, а у PHP интерпретатора уже будут исчисляться мегабайтами, может быть парой десяток мегабайт памяти на 1 запрос.

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

        Самое читаемое