株式会社メドレーDeveloper Portal

2018-08-27

HTTP Cache で求人サイトのスピード改善を試してみた話

こんにちは、開発本部の楊です。メドレーの社内勉強会「TechLunch」で、前回は、React の基本を紹介しましたが、今回は HTTP Cache で、医療介護求人サイト「ジョブメドレー」のスピード改善ができないか検討した話について、共有しました。

なぜ HTTP Cache について話すことにしたのか

ジョブメドレーには、医療機関や保育園、介護施設などさまざまな事業所の求人が掲載されています。現在、14 万を超える事業部の求人が掲載されており、かつ事業所側で求人原稿を修正することもできるため、求職者側が閲覧するスピードについては、いつも意識して改善に取り組んでいます。

その試行錯誤の中で、HTTP Cache を使って改善する方法について、実現性など含めて検証することにしました。

今回は、あるページをモデルケースに試してみました。このページは、PC は平均 250ms、モバイルは 180ms になっています。

20180824171410.png

ユーザが該当のページを見たときに「内容に更新がないときはブラウザ側のキャッシュを利用してスピードを最適化する」「ページが更新されていたら、最新の内容をすぐユーザへ反映する」という要件でスピード改善されることを目指すことにしました。

HTTP Cache とは?

Google Developersでは以下のように説明されています。

ネットワーク経由で情報を取得するには時間もコストもかかります。レスポンスが大きいと、クライアントとサーバ間のラウンドトリップを何度も繰り返す必要があるため、レスポンスが利用可能となってブラウザで処理できるようになるまで時間がかかります。さらに、ユーザ側ではデータの通信コストが発生します。そのため、前に取得したリソースをキャッシュに保存して再使用できることは、パフォーマンスを最適化する上で非常に重要です。

この機能はほぼ全てのブラウザに対応しており、HTTP ヘッダーでCacheControlETAGLast-Modifiedなどを利用して、リソース更新するタイミングなどを細かくコントロール可能です。

Rails での HTTP Cache

expire_in でキャッシュ

1 時間キャッシュしたい場合は、コントローラにコードを一行書けば大丈夫です。

expires_in(1.hour)

これで問題なく 1 時間キャッシュされるのですが、「ページが編集されたら即時にユーザへ反映する」という要件を満たしてはいません。

ETAG を利用

では「ページが更新されたら最新の内容をすぐユーザへ反映して、更新がない時はブラウザ側のキャッシュを利用してスピードを最適化する」を行いたい場合はどうすればよいでしょうか。 ここではETAGを利用しようと思います。

ETAGはページの内容によって、ユニークな文字列を作成して、ブラウザ側でページの更新あるかどうかを判定するものです。

ETAG はどう作成されているか

Rails のデフォルトでは HTTP Cache を有効になっていて、ETAGを自動的に作成しています。

header['ETag'] = Digest::MD5.hexdigest(body)

上のコードのようなイメージで、レスポンス Body からETAGを作成します。

つまり、サーバから返される HTML ソースの内容が毎回同じであれば、ブラウザは前回キャッシュした内容を読み込むようになります。

サーバレスポンスタイムの短縮

今回の対象ページの機能としてサーバ側では主に二つの部分で時間がかかります。

  1. DB、Redis などで画面表示が必要なデータ取得、ロジック処理
  2. クライアントに返す HTML のレンダリング

対象ページは「HTML のレンダリング」する時間が長かったので、それを改善できれば、サーバレスポンスタイムを一気に短くできます。

画面に表示する必要なデータに変換がなければ、HTML のレンダリングをせずにレスポンスを返す機能がないかをさらに調べました。

fresh_when を使う

名前の通り、いつ画面更新するかをコントロールするメソッドです。

データベース中の該当データが更新されたら、新しいETAGを作成するコードは以下のようになります。

fresh_when(etag: [@job_offer, @job_offer.facility])

model から ETAG をどう作成するか

model の cache_key から ETAG を作ります。

該当ページで使用する job_offer モデルの cache_key は"job_offer/5-20071224150000"のような感じになります。

model の id と updated_at の組み合わせでユニークな cache_key を作成しています。

Rails の該当コード

def cache_key(*timestamp_names)
  case
  when new_record?
    "#{model_name.cache_key}/new"
  when timestamp_names.any?
    timestamp = max_updated_column_timestamp(timestamp_names)
    timestamp = timestamp.utc.to_s(cache_timestamp_format)
    "#{model_name.cache_key}/#{id}-#{timestamp}"
  when timestamp = max_updated_column_timestamp
    timestamp = timestamp.utc.to_s(cache_timestamp_format)
    "#{model_name.cache_key}/#{id}-#{timestamp}"
  else
    "#{model_name.cache_key}/#{id}"
  end
end

自前の作成したクラスから ETAG をどう作成するか

model だけではなく、自前で作成したクラスでデータ管理をしてるところもあります。 そちらでの実現方法も試してみました。

そこでETAGを作るコードが Rails でどうなっているか追ってみました。

Rails の該当コード

def retrieve_cache_key(key)
  case
  when key.respond_to?(:cache_key) then key.cache_key
  when key.is_a?(Array)            then key.map { |element| retrieve_cache_key(element) }.to_param
  when key.respond_to?(:to_a)      then retrieve_cache_key(key.to_a)
  else                                  key.to_param
  end.to_s
end

このように、自作クラスにcache_keyのメソッドを定義したら、その結果から ETAG が作成されるようになっています。このメソッドを使って、ユニークな値を返せば大丈夫です。

ここまで調査したことを踏まえて、該当ページを HTTP Cache で実装をしてみました。以下が該当の疑似コードになります。

class JobOfferBrowsingHistory
  def cache_key
    # return job offer ids
  end
end

fresh_when に渡す

@user_history = JobOfferBrowsingHistory.new
fresh_when(etag: [@job_offer, @job_offer.facility, @user_history])

テスト

開発環境で該当ページを 2 回ロードしました。 1 回目は 1200ms、キャッシュが効いた 2 回目は 130ms になりました。すごく早いです! もちろん、ページ内のデータが更新されたら、最新の内容がページに反映されるようにもなっています。

これで「ページのデータが更新されたら、最新の内容をすぐユーザへ反映する」「更新がなければ HTTP Cache を返す」という状態が実現しました。

課題

  • 実装ミスで、更新が必要なのに、更新されない問題が起こりえる
    • 例えば、画面に新しい model を追加したが、fresh_whenに渡すのを忘れたとか
  • 問題なく更新されることを保証する仕組みの実装

まとめ

HTTP Cache を利用して、ジョブメドレーのスピード改善を検討してみた調査過程を紹介しました。

expire_inETAGの作成方法など色々調べて、最終的にfresh_whenで実現できましたが、運用していく上での課題については、引き続き検討して行きたいと思います。

株式会社メドレーDeveloper Portal

© Medley Developer Portal