| @技術/プログラミング

OGP 読み込み君が悪用されていて困ったという記事を書いた。

Referer でブロックされるようなサイトからリンクするときに OGP 読み込み君をかませて掲示板やブログのコメント欄に URL を投稿しまくっていたのだと思う。調べたら 73 万件以上のリンクが OGP 読み込み君によって生成され、リンク先の OGP カードがキャッシュされていて、ディスク容量を 2.8GB も消費していた。許せん。

認証なしで使える OGP 生成カードのようなエンドポイントを露出していたのがそもそもの間違いだった。スパマーのはびこる今日のインターネットではこういう無防備なことをはするべきではなかった。

ということで iframe で OGP を展開する方式をやめて、インライン表示するようにした。普通に OGP 表示用の HTML を生成してキャッシュしている。なので初回に OGP を読み込むときにめっちゃサイトのレスポンスが遅くなった( iframe であれば非同期読み込みできてメインの HTML の描画は高速だった)。

良くも悪くも、インターネットは巨大になってきていて、ちょっと穴のあるシステムをインターネットに公開してしまうと悪意をもった人に悪用されて膨大なダメージを負ってしまう可能性がある。難しい世の中になってきている。

| @技術/プログラミング

キャッシュ

CloudFront 転送量削減の試みで色々やっていて、そういやウェブサイトのキャッシュについての説明記事っぽいの読んだことないし、開発現場だと「じゃあキャッシュすればいいじゃん」みたいな発言が行き交うけど、ひとくちにキャッシュと言っても色々あって、ウェブ開発始めたばかりの人にはよく分からないんじゃないかと思ったので書いておきます。

キャッシュの目的

キャッシュの目的.png

キャッシュの目的は大きく二つあって、コンテンツ表示の高速化とコストの削減がある。コンテンツの表示高速化は、時間のかかる処理の結果を捨てずに再利用して二回目以降の表示を高速化すること、コストの削減は、処理結果の再利用によってコンピューターの利用時間を減らしたり、データの通信量を減らすことをそれぞれ目的としている。表示高速化とコスト削減はどちらか一方だけを達成するものではなく、多くの場合で両方が同時に実現される。

キャッシュに向いているコンテンツ

キャッシュには向いているコンテンツと向いていないコンテンツがある。

キャッシュに向いているコンテンツ.png

ウェブサイトで配信するものに関して何でもキャッシュできるわけではなく、多くの人が共通して閲覧するものか、更新頻度が低いものでないとキャッシュしてはいけない。更新頻度が低くて多くの人が共通して閲覧するもの(画像、 CSS 、 JavaScript など)はキャッシュしやすい。だがショッピングサイトの購入履歴などは人それぞれなので一律にキャッシュしてはいけない。

キャッシュの種類

キャッシュにはブラウザーキャッシュとサーバーサイドのキャッシュがある。

キャッシュの種類.png

ブラウザーキャッシュ

ブラウザーキャッシュは、同じ人が繰り返し訪問するサイトで繰り返し利用されるサイト内画像や JavaScript 、 CSS (静的コンテンツ)に対して提供するのに向いている。二回目以降のアクセス時に前回端末にダウンロードした内容を使い回し、転送量を削減してユーザーの閲覧体験を高速化する。

ブラウザーキャッシュ図解.png

同じ人が繰り返し訪問しないタイプのサイト( SNS でバズって多くの人が訪れるが、一ページ見ただけで離脱するようなサイト)ではブラウザーキャッシュをフルに効かせても転送量の削減効果はない。逆に一度訪れた人が何度も訪れるような再訪率の高いウェブサイトでは、静的コンテンツに対して Cache-Control ヘッダーを付与することでブラウザーキャッシュを効かせ、転送量の削減とコンテンツの表示速度を高速化させることができる。

ブラウザーキャッシュを聞かせるには、適切な HTTP ヘッダー( Cache-Control ヘッダーなど)を付けてレスポンスを返せばよい。

サーバーサイドキャッシュ

サーバーサイドでキャッシュできるのは多くの人が共通して閲覧する、更新頻度の低いデータだけということに注意が必要。ログインが必要なサイトで、アクセスする人によってコンテンツを出しわけないといけないようなシステムではサーバーサイドのキャッシュはあまり利用できない。

サーバーサイドキャッシュ図解.png

サーバーサイドのキャッシュには転送量の削減効果はない(少なくとも自サイト訪問者との通信では)が、ランキング集計処理や外部 API の呼び出しなど CPU への負荷や時間がかかる処理の結果をキャッシュし、コンテンツの表示を高速化したいときに有効。

例えばこのサイトでは Amazon のアフィリエイトを利用しているが、 Amazon の API にリクエスト回数制限やリクエスト間隔制限があるので適度にキャッシュを行い、リクエスト回数制限に引っかからないようにしている。

サーバーサイドのキャッシュのやり方は色々ある。アプリケーションにキャッシュするためのコードを書いてメモリ上に保持したり、テキストファイルに保存したり、データベースに保存したり、 Redis などを使ったり。ウェブサーバーのキャッシュを使う方法もある。このブログでもいくつかのキャッシュ機構を組み合わせている。

アプリケーションキャッシュ.png

表示に関わる部分の一部をキャッシュする場合や、キャッシュの無効化をアプリケーションでコントロールしたい場合はアプリケーションでキャッシュする必要がある。

ウェブサーバーキャッシュ

レスポンス全体をキャッシュしてよい場合にはウェブサーバーのキャッシュを使うとよい。その方がアプリケーション層までリクエストが届かず、コンピューターリソースを節約できる。

様々なキャッシュの組み合わせ

更新頻度が低く、多くの人が共通して閲覧するコンテンツはサーバーサイドでもキャッシュできるしブラウザーキャッシュを効かせることができる。画像、 CSS 、 JavaScript などがそれで、これらのファイルはよく CDN ( Content Delivery Network )から配信される。 AWS だと CloudFront というのがある。この手のサービスはサーバーサイドのキャッシュとブラウザーキャッシュを効かせることに特化したもので、コンテンツの転送量を抑えつつウェブサイトの表示速度を高速化してくれる。

CDN もそうだが、サーバーサイドキャッシュとブラウザーキャッシュを併用すると、コンテンツを更新したいときに問題が出てくる。画像ファイルを新しいものに差し替えたが CSS が更新されず古いキャッシュが参照され、画像が非表示になってしまったり、ということが発生する。そういうことが起こらないように、 HTTP にはいくつか仕組みがある。

CDN だと ETag も自動付与してくれて、コンテンツの衝突を抑えてくれる。 CDN はよくできているのでよく分からない人はまずは CloudFront を使って勉強してみるとよいだろう。

特に注意が必要なのがアプリケーションキャッシュとウェブサーバーキャッシュの組み合わせだ。異なるレイヤーでキャッシュを効かせてしまうと、キャッシュを無効化したいときに狙い通りに無効化されず、意図しない障害になってしまったりする。コンテンツの特性に応じて、キャッシュは一つのレイヤーで行うようにするとよいだろう。画像や CSS はウェブサーバーでキャッシュし、更新頻度が低いが動的に生成される JSON はアプリケーションレイヤーでキャッシュするなど。

まとめ

  • ウェブサイトのコンテンツは何でもキャッシュすれば良いわけでない
  • サーバーサイドのキャッシュとブラウザーキャッシュの違いを理解する
  • 多段キャッシュに注意(特にアプリケーションキャッシュとウェブサーバーキャッシュの併用)

| @技術/プログラミング

関連記事に画像を表示するようにして喜んでいたが、先月の AWS の請求額を見てビックリ。普段の 15 倍くらいの金額になっていた。デイリーの利用料金を見ると関連記事に画像を表示するようになった日から高くなっている。

CloudFront 転送量

このブログの画像は S3 に置いてあって CloudFront から配信している。これまでたくさん写真を掲載しても特にコストは高くなかった( Route 53 の費用など含めても $3 くらい、転送量だけだと $1.5 くらいだった)のが、転送量だけで $30 オーバーになっていた。ブログのサーバー代は Adsense 広告と Amazon アフィリエイトでまかなうつもりでやっているので、これでは完全に赤字になってしまう。

なぜ高くなったのかというと関連記事にサムネイル画像を表示することで、 imageproxy から CloudFront へのアクセスが発生するようになったからのようだった。こんな感じ。

image-data-transfer-infrastructure-1.png

imageproxy にもキャッシュの仕組みはあるが、 CloudFront が返す Cache Control ヘッダーの内容を理解せず決め打ちの時間でキャッシュを Expire させるので効率が悪い。

恐らく以下のように画像関連のインフラは AWS に寄せるのが一番効率的だと思う。Amazon の優秀なエンジニアが作ってる CDN が一番前段に出てブラウザーからのリクエストに答えるのがもっとも効率的に画像を配信できると思う。

image-data-transfer-infrastructure-2.png

ただ個人のブログレベルでここまでやるのは割に合わない感じがしたのでとりあえずは以下のような構成にした。

image-data-transfer-infrastructure-3.png

Nginx の proxy cache を使う。

キャッシュ時間は長めにとって 30d にしておいた。

あわせてキャッシュの HIT 率を計測するようにした。ログに $upstream_cache_status を書き出すようにして、 awk で定期的に集計するようにした。こんな感じ。

cat log/access.log \
  | grep 'cache_hit:' | grep -v 'cache_hit:-' | cut -f16 | sort | uniq -c \
  | awk '{
      if ($2 ~ /HIT/) {
        hit = $1
      };
      if ($2 ~ /EXPIRED/) {
        expire = $1
      };
      if ($2 ~ /MISS/) {
        miss = $1
      };
      sum += $1
    } END {
      hit_rate = hit/sum*100;
      expired_rate = expire/sum*100;
      miss_rate = miss/sum*100;
      print "HIT\t"hit_rate"%\nEXPIRE\t"expired_rate"%\nMISS\t"miss_rate"%"
    }'

こいつを Lokka の Dashboard に表示させる。

キャッシュヒット率

加えて、 Google の以下の記事を参考に、画像の遅延読み込みを行うようにした。

とりあえずはこれで様子を見たい。いまのところ、ちょびっとずつ転送量は下がってきているような感じがする。もうちょい下げたいところ。

しかし、画像の配信で毎月 $30 もかかるようであれば自前で画像をホストするのは諦めて Flickr に金払って PRO プランを継続した方が安いなと思い始めてしまった…。 Google Photos でも良いが、 Exif がわからなくなるのと埋め込み用の画像を取得する作業(公開用のアルバムを作ってそこに埋め込みたい写真を入れていく必要がある)が面倒くさいので移行に踏み切れない。

| @WWW

飾り瓦

以前、 OGP を読み込んでキャッシュする仕組みを作ってたけど、こいつをアップデートして iframe として静的な HTML を読み込むバージョンに作り直した。 URL をクエリパラメーターとして渡すと相手方のサイトにアクセスして OGP を読みに行き、プレビュー用の HTML を生成してキャッシュする。いまは Lokka Plugin として作ってるけどこいつはブログアプリケーションと密結合する必要はないので独立した Web アプリケーションにしてもよいかもしれない。

ブログにリンクを張ると OGP を展開する仕組み、いろんなサイトやサービスで独自実装されててもったいないと思う。 Facebook が OGP の仕様を作ったけど利用するかどうかはリンク元次第だし、 Twitter は Twitter Card という独自の仕組みを作ってる。この辺はいい感じに一本化して欲しさがあるが、大人の事情で多分できないだろう。

ということでグローバルな OGP 君があったら良いだろうと思う。 OGP 君は一枚噛ませるだけで OGP 関連の面倒なこと(リンク先サイトのOGP タグ読み込み、 OGP によるプレビュー生成、生成したパーシャル HTML のキャッシュ)などをやってくれる。様々なサイトでキャッシュが共有されるのでインターネット資源が有効活用される。OGP 君運営者は様々なサイトに script タグなり iframe タグなり1を埋め込めるので、そこでサイトの利用状況などを副産物としてゲットすることができる。 Google あたりだったらこの辺の情報を金にできそう。

ちなみに同じようなことを考えた人はすでにいて Embedly というサービスがあるようだが、これはリンクを張る側からお金を取る仕組みのようでいまいちイケてないと感じる。見栄えの良いリンクにしたいのはリンクする側ではなくどちらかというとされる側なはずなので、リンクされる側からお金をもらうような仕組みの方が良いはず。


  1. iframe はセキュリティ上、異なるドメインのものを埋め込むのはまずかった。やるなら script タグで動的に DOM を生成するタイプのものだろうなぁ。 

| @ブログ

関連記事

各記事の一枚目の画像をカバー画像とみなすようにして、関連記事にサムネイルを表示するようにしてみた。画像があるだけで記事をクリックしてみたい感が高まると思う(残念ながら Google Analytics で見る限り直帰率は改善してない)。人間は、文字だけを読むよりもイラストや画像など視覚的な情報を一緒に見ることで物事の理解度が高まると本で読んだ。自分で過去記事を読み直していても画像があると関連記事をクリックしてみたくなるし、満足感のある改修だった。

なおカバー画像の判定処理は記事本文を読み込んだ上で正規表現で調べているので多分重い(ベンチマークを取ったらタイトルを表示する処理に比べて二倍くらい遅かった)のだけど、関連記事表示部分はキャッシュしてるのでそんなに体感速度は悪化してないはず。