CloudFront(以下CFと略)を始め、CDNを利用するにあたり、まず注意することは、キャッシュを削除する手段
を確立することです。CFはinvalidationをURL毎に発行することにより、このキャッシュクリアを実現していましたが、昨年、ワイルドカード (e.g /image/*) で一気にURLを指定し、クリアすることができるようになりました、が、一部の環境において不具合があり、それをAWSサポートに連絡、修正してもらいました。今はすでに治っているので問題ありませんが、CFの理解のために記述しておきます。
不具合ってどこよ?
この問題を 勝手に古いヘッダのキャッシュ問題
と呼称します。
発生のメカニズムは
- CFにキャッシュされる、このとき、レスポンスヘッダもキャッシュされる
- TTLが切れて、オリジンにリクエストが飛ぶ
- オリジンのヘッダだけが変わっていて、コンテンツに変化がない場合、オリジンは304を返す
- CFは304を受けると、1. でキャッシュしたデータをつかいまわす
これに対して invalidationをかければOK!なんですが、ワイルドカードでinvalidationを掛けた場合、上手くクリアできません。
ヘッダだけが変わるってどういう状況?
CORSです。CORSの設定を忘れて、CFにキャッシュされたあとに、オリジンにCORS向けヘッダを指定したらこの状況になります。
CORSってなんや?
長くなるので適当に AJAXなどで、動的に別のURLの画像やコンテンツを取得し、使う場合に発動するブラウザ側のセキュリティーチェック
と考えてください。
「えっ、Yahooとかでも外部サイトの広告とか出てるやん」と思うでしょうが、あくまでも動的に
やる場合です、HTMLにベタでimageタグ書く場合はCORS関係ないです。ちょー適当にいうとWeb2.0です。知らないと生きていけない知識ですので、はじめて知った人はググってください。
今回はGETのことだけ考えていますが、POSTの場合でもCORSはあります。プリフライトリクエスト
でググってください。こちらはちょっとむずかしいですので、私のボケ防止にいつか書く。
CFのキャッシュの一生
CFは特定のURLリクエストを受けると、下記の動作をとります。
- CFにキャッシュが無い場合は、オリジンに取りに行く
Miss from cloudfront
- CFにキャッシュがあり、且つTTLが切れていない場合は、キャッシュから返す
Hit from cloudfront
- CFにキャッシュがあるが、TTLが切れている場合は、再度オリジンに取りに行く
RefreshHit from cloudfront
上の2つはだいたいわかると思いますが、最後のRefreshHit from cloudfront
の時に 古いヘッダのキャッシュ問題
は発生します。
TTLが切れたらキャッシュは破棄されるのではないのか?
破棄されません。破棄されるとおもいますよねーふつう。原理的にこれがあるので、古いヘッダのキャッシュ問題
が発生するのです。
ということは 古いヘッダのキャッシュ問題
は放っておいたらずっと解消しない?
確率としては極めて低いですが、解消する可能性はあります。
CFは RefreshHit from cloudfront
状態で実はキャッシュは捨てていないのですが、キャッシュ対象のURLに一定時間以上、アクセスが発生しない場合は、完全にキャッシュ削除されるタイミングがあります。しかしこのタイミングは完全にAWS様の機嫌次第です。何分とか明言されておりません。またTTLが1日とかの場合は挙動が変わるかもしれません(試してないです)
検証
状況を整理しておきます。
古いヘッダのキャッシュ問題
そのものは修正されていない。個人的にはTTL切れ = キャッシュ破棄としてもらうほうがいいので、改善要望は出した。古いヘッダのキャッシュ問題
のW/Aは、invalidationすること。- しかし 個別URLで invalidation した場合は綺麗に
Miss from cloudfront
になるが、ワイルドカードでinvalidation した場合は、RefreshHit from cloudfront
になる(キャッシュ破棄が発生しない)。
今回の問題は、ワイルドカード invaliだと古いヘッダのキャッシュ問題
のW/Aが効かない(かった)、ということです。しかもこの問題はTTLが十分に長い設定(e.g 1day)などの場合は発生しません。私の検証では、Hit from cloudfront
状態つまり、TTLが切れていない状態で、ワイルドカード invaliを掛けた場合は、なぜかMiss from cloudfront
になりますつまりW/Aが効いて、キャッシュが破棄される。TTLを1分とか短い設定にしていると、必然的にRefreshHit from cloudfront
になりやすく、この状態でのW/A(ワイルドカードでinvaliをかける)が効かないということがわかっています(た)。
手順概要
- S3にダミーコンテンツをぶち込む
- S3にCORSの設定を
しない
- CFをS3オリジンで作る、TTLは30秒とか短くする
- CFにキャッシュを吸わせる =
古いヘッダのキャッシュ
を作る - S3にCORSの設定を
する
- CFに問い合わせて
古いヘッダのキャッシュ
問題の発生を確認 - ワイルドカードで invaliかける
- CFに問い合わせて
Miss from cloudfront
を確認 (不具合修正の確認)
検証スクリプト
S3にダミーコンテンツをぶち込むやつ
#!/bin/env ruby require 'aws-sdk' BACKET_NAME = 'YOUR Bucket Name' def put_3k_files (d, ver) s3 = Aws::S3::Client.new(region: 'YOUR Region') for num in 0..1000 do key = d + sprintf("/%04d.txt",num) s3.put_object(bucket: BACKET_NAME, key: key, body: 'this is ' + key + 'ver ' + ver) end end put_3k_files 'js', '1' put_3k_files 'js/001', '1' put_3k_files 'js/001/a', '1' put_3k_files 'js/001/b', '1' put_3k_files 'sound/prd', '1' put_3k_files 'asset', '1'
S3を突っつくやつ
#!/bin/env ruby require 'net/http' require 'uri' require 'resolv' def get_url(ac_url) url = URI.parse(ac_url) req = Net::HTTP::Get.new(url.path) req.add_field 'Origin', 'http:example.com' req.add_field 'Host', 'CF FQDN' res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req)} ret_arr = [7] ret_arr[0] = ac_url ret_arr[1] = res.code ret_arr[2] = "None" res.each do |n, v| if n == 'access-control-allow-origin' ret_arr[2] = v end if n == 'content-length' ret_arr[3] = v end if n == 'x-cache' ret_arr[4] = v end if n == 'etag' ret_arr[5] = v end if n == 'age' ret_arr[6] = v end end return ret_arr.join("t") end rslv = Resolv::DNS.new() ip_list = rslv.getaddresses('CF FQDN') dirs = ['js','js/001','js/001/a','js/001/b','sound/prd','asset'] #dirs = ['js'] for num in 0..1000 do key = ARGV[0] + sprintf("/%04d.txt",num) full_url = 'http://' + 'CF FQDN' + "/" + key print get_url(full_url) + "n" # ip_list.each do |ip| # full_url = 'http://' + ip.to_s + "/" + key # print get_url(full_url) + "n" # end end
コードは散らかっているが、引数に js とか asset とか指定すると、その配下の1ファイルを突っつきます。出力の3カラム目に *がきていれば CORS用のヘッダが来ています。コメントアウトしてますが、DNSラウンドロビンで返してくるIP全てに対しても打つ事ができます。(あたりまえですが、めちゃ時間かかります)興味ある人は試してみてください。
検証結果
問題は確かに解消していました。
まとめ
ワイルドカードによるinvalidationの問題は解消されましたが、基本的にinvalidation自体が悪手
です。さらに、前述してますが、TTLが切れたらキャッシュも削除される
という認識が根本的に間違っています(いいか悪いかを別にして、CFにおいては)。さらにさらに、TTLさえ短ければキャッシュ残りは心配しなくていいという考えも間違っています、レスポンスヘッダとコンテンツをCFはごちゃまぜでキャッシュしています。
よって、古典に習い
- 先頭URLをバージョンとかにして、バージョンごとにURLを使い捨てる
- QueryString等でごまかす
を使うのがベストです。別URLはCFは別キャッシュだと判断するので、オリジンが同じであろうと関係なく、URLが違えば別キャッシュとして扱う
と覚えておきましょう。ちなみにリクエストヘッダ
もCFが同じキャッシュとみなすか否かに含まれているようです(試してみてください)。少し話がそれましたが、invalidationはおまけ
ぐらいに心構えるのが良いです。
.. あと、この手の検証をやる人は、ブラウザ側のキャッシュを強制無効にしてやるのがいいです。