更新するたびにキャッシュがクリアされる?「node_list」キャッシュタグにご用心

花岡 重宏
アイキャッチ

Drupal 8からはキャッシュの仕組みが大きく変わりました。
具体的には、下記のような「キャッシュメタデータ」と呼ばれるデータを利用しつつ、キャッシュを細かく生成するようになっています。

メタデータ 説明 利用方法
キャッシュタグ(cache tag) キャッシュがどのタイミングでクリアされるべきかを表す 特定の操作をしたときにキャッシュをクリアするために利用する
キャッシュコンテキスト(cache context) キャッシュのバリエーションを表す ユーザーや言語、パス、クエリパラメータなど条件に応じて異なる内容をキャッシュしたい場合に利用する
キャッシュ最大保持時間(max-age) キャッシュの期限を表す 一定時間経ったらクリアしたい場合に利用する


そして更新があった場合は、影響を受けるキャッシュだけをクリアします。
こうすることで、クリアするキャッシュが最小限で済むため、キャッシュの恩恵を最大限に受けられます。

クリアするキャッシュが最小限で済むイメージ
 

ダイナミックページキャッシュモジュールがデフォルトで有効になっていますが、これによりログインユーザーの画面でも適切な形でキャッシュが行われ、単純な構成のサイトであれば特別な設定なく非常に軽快に動作します。

このように、Drupal 8ではサイト全体を通して無駄のない、一貫性のあるキャッシュシステムに進化しています。
しかし、ある程度大きな規模のサイトになってくると、調整が必要な場合も出てきます。

実際に大規模なサイトで、キャッシュが原因で「パフォーマンスに影響が出る現象」が起きたので、この記事ではその原因と解決方法についてご紹介します。
 

本番環境でのみ表示速度が重くなる現象

今回起きた現象は「本番環境で表示がなんとなく重い(時間帯によってはかなり重くなる)」というもので、サイト状況は下記の通りです。

  • 大規模で構成も複雑
  • カスタマイズが大量に入っている
  • ステージング環境ではサクサク動くが、本番環境では重くなる

本番環境でページキャッシュが効いているかどうか確認してみたところ、キャッシュが効いているときと、効いていないときがありました。

また、本番環境では1分もしないうちにキャッシュが切れることが多く、ステージング環境では普通にキャッシュが効いています。

本番環境とステージング環境の違いは「実際にユーザーが使用しているかどうか」ということから「ユーザーのアクションが原因で発生している現象ではないか?」と推測できました。
 

原因は「node_list」キャッシュタグ

このサイトでは10万人以上のユーザーアカウントがあり、サイト全体で1分間に平均3回コンテンツが更新・作成されている「非常に更新の多いサイト」でした。

そして、Drupalのコンテンツリストの「Views」ビューには、デフォルトで「node_list」というキャッシュタグが入っていました。

この「node_list」の影響で、頻繁にキャッシュがクリアされてしまい、サイトが重くなっていました。
 

キャッシュタグとは

Drupalは多くのレイヤーで様々なキャッシュを生成しています。

  • ページキャッシュ
  • レンダーキャッシュ
  • 処理結果のキャッシュ

1つ1つのキャッシュには「キャッシュタグ」というものが付いています。
これは、サイト内で「特定の操作をしたらクリアすべきキャッシュだよ」と示すものです。

たとえば「user:123」というキャッシュタグは、「ユーザーIDが123のユーザーに、変更があった場合にクリアするキャッシュ」だと示しています。

実際にユーザーIDが「123」のユーザーが、プロファイル画像やユーザー名などを変更したときに、「user:123」が付いているコンテンツのキャッシュはすべてクリアされます。
 

「node_list」キャッシュタグは影響範囲が大きい

このように「user:○○」は特定ユーザーが関わっているコンテンツにだけ影響しますが、それに対して「node_list」の影響範囲はかなり広く、サイト内に存在するノードが更新・追加・削除されたときにキャッシュがクリアされます。

つまり「node_list」の付いたキャッシュは、Drupalで更新されるたびに反応するため、キャッシュがクリアされる頻度が非常に高いということです。
node_listはクリアされる頻度が高い

そのため、頻繁にノードが更新・追加されるサイトでは、「node_list」キャッシュタグは除外する必要があります。
「node_list」キャッシュタグの代わりに、もっと範囲を絞った限定的なキャッシュタグを使用しないと、キャッシュが頻繁にクリアされてしまうからです。
 

「node_list」による問題は現在改修が進められている

ちなみに、このように頻繁にノードが更新・追加されることでパフォーマンスに影響が出る問題に関しては、Drupalのコアモジュール改修が進められています。

2022/04/17現在のステータスは「Needs review」なので、もう少し待っていれば改善されて特に影響が出なくなりそうです。

参考:Use new cache tag ENTITY_TYPE_list_BUNDLE in Views to improve cache hit rate 

現時点では、このパッチを適用して、動作的に問題なければそれで対応するのも良いかもしれません(開発中のdevバージョンなので自己責任でお願いします)。

パッチの適用以外で現在できる改善方法については、この先の内容を参考にしてください。
 

現在できる改善方法

現状の確認

ブラウザの開発者ツールで、レスポンスヘッダーを確認します。
Google Chromeの場合は、開発者ツールの[ネットワーク]タブから、リクエスト名を選択し、右側の[ヘッダー]のところでレスポンスヘッダーを確認できます。

「X-Drupal-Cache-Tags」の項目で、そのページに適用されているキャッシュタグが確認できます。
(※services.ymlのhttp.response.debug_cacheability_headersの値をtrueにしておかないと表示されません)

インストールしたばかりのプレーンなDrupalで見たところ、下記の内容が確認できました。
ブラウザのデベロッパーモードでの閲覧イメージ

X-Drupal-Cache-Tags: block_view config:block.block.bartik_account_menu config:block.block.bartik_branding config:block.block.bartik_breadcrumbs config:block.block.bartik_content config:block.block.bartik_footer config:block.block.bartik_help 
…
~~ 中略 ~~
…
config:user.role.content_editor config:views.view.frontpage http_response local_task node:1 node:2 node_list node_view rendered user:1 user_view

「node_list」が含まれているので、これを除外するのが今回の目的です。

 

「node_list」キャッシュタグを除外する

まずは今回のパフォーマンス低下の原因である「node_list」キャッシュタグを除外します。
下記のようにカスタムモジュールに「hook_views_post_render()」を実装します。
 

use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\cache\CachePluginBase;
/**
 * Implements hook_views_post_render().
 */
function mymodule_views_post_render(ViewExecutable $view, array &$output, CachePluginBase $cache) {
  switch($view->id()) {
    case 'my_test_view':
      // キャッシュタグから node_list を除外する
      if (!empty($output['#cache']['tags'])) {
        $key = array_search('node_list', $output['#cache']['tags']);
        if ($key !== FALSE) {
          unset($output['#cache']['tags'][$key]);
        }
      }
      break;
  }
}

これでビューから「node_list」キャッシュタグが除外されます。
除外されたかどうか、開発者ツールから「レスポンスヘッダー」を見て確認しましょう。

もし、まだ「X-Drupal-Cache-Tags」に「node_list」が残っている場合は、他の要素に「node_list」が残っている可能性があります(サイドバーの一覧ブロックなど)。
その場合は該当の要素を探し、除外してください。

Drupalには、「特定要素のキャッシュタグは、最終的にページ全体のキャッシュタグに反映される」という仕組みがあります。
そのため、ページ全体から特定のキャッシュタグを除外するためには、そのページに含まれるすべての要素からキャッシュタグを除外する必要があります。
 

特定のコンテンツタイプの更新でクリアする

これで「一覧リストのキャッシュがノード更新のたびにキャッシュクリアされる」ことは回避できました。
しかし、これではその一覧リストに関係のあるコンテンツが更新された場合もキャッシュクリアされなくなってしまいます。

必要なタイミングで正しくキャッシュクリアが行われ、一覧も更新するためには、Drupal 8.9から標準で実装された「node_list:(コンテンツタイプ名)」というキャッシュタグが使用できます。

たとえば、newsコンテンツタイプを一覧に表示しているビューについては、「node_list:news」というキャッシュタグを付与します。

付与すると、newsコンテンツタイプのノードで更新があった場合に、一覧のキャッシュがクリアされます(news以外のコンテンツが更新された場合はキャッシュはクリアされません)。

実装方法は、先ほどの「hook_views_post_render()」に下記コードを追加します。
 

// 「ニュース」コンテンツタイプの更新でキャッシュクリア
$output['#cache']['tags'][] = 'node_list:news';


もしくは、「Views Custom Cache Tagsモジュール」で、ビューの編集画面からタグを追加することもできます(2022/04/17現在、特定のタグの追加はできても除外はできません)。

 

まとめ

今回の案件では実際に「node_list」キャッシュタグを除外したことで、毎分3回ほどのキャッシュクリアがなくなり、一定の効果が確認できました。

キャッシュタグについて今回は「特定のコンテンツタイプの更新」でクリアする解決策を紹介しました(これはもうすぐ、コアに実装されそうです!)。
他にも、さらに限定した「特定条件のノードが更新されたときだけに影響されるカスタムキャッシュタグの実装」も可能です。
そのあたりはまた機会があったら紹介したいと思います。

花岡 重宏/ Drupalエンジニア

Drupal が恋人です。開発側から見えてくるものも沢山あるので、チームメンバーと協力しながら、最終的にお客様も満足するモノづくりを目指しています。
週末は散歩したり、Drupalと遊んだりしています。

花岡 重宏 の書いた記事一覧

最新の関連記事

Contact お問い合わせ

Drupalでの開発・運用、サーバー構築、Webサイト構築全般、制作費用などに関してお気軽にご相談ください。