ほとんどのブラウザで、通常リロードは Cache-Control: max-age=0、スーパーリロードで Cache-Control: no-cache がリクエストヘッダとして送られてくるみたいなのですが、実際の挙動はともかく意味の違いがよくわかりませんでした。

RFC7234 によると max-age は

The "max-age" request directive indicates that the client is unwilling to accept a response whose age is greater than the specified number of seconds. Unless the max-stale request directive is also present, the client is not willing to accept a stale response.

https://tools.ietf.org/html/rfc7234

no-cache は

The "no-cache" request directive indicates that a cache MUST NOT use a stored response to satisfy the request without successful validation on the origin server.

https://tools.ietf.org/html/rfc7234

となっています。読んでも違いがよくわかりません。検索するとWhat's the difference between Cache-Control: max-age=0 and no-cache? あたりがヒットして、古いほうのRFC HTTP/1.1: Caching in HTTPが示されています。

max-age=0 は validate した結果最新とわかったならキャッシュを返すことを許しているが、no-cache はそもそも cache を使うことを許していない、という違いがあるようです。

もっというと、max-age=0 のときは validate して最新なら 304 を返してくれてもいいが、no-cache のときは validate すらせず全て 200 で転送しなおせという意味のようです。

余談:レスポンス時の Cache-Control: no-cache

レスポンス時の Cache-Control: no-cache が語感に反してキャッシュの利用を許可しているというのは良く知られている(?)と思いますが (キャッシュの利用を許可しないのは no-store)、これも max-age=0 との違いがよくわかってませんでした。

これについても上記の stackoverflow でわかりやすい解説があって、Cache-Control: no-cache は実質 Cache-Control: max-age=0, must-revalidate 相当だろうとのことでした。単純な max-age=0 はクライアント側が validate できなければ期限切れのキャッシュ利用を許しているという違いがあるようです。

  1. トップ
  2. tech
  3. リクエスト時の Cache-Control、max-age=0 と no-cache の違い

Hardware IO Tools for Xcode をダウンロードすると Network Link Conditioner という環境設定が含まれていて、インストールするとそこそこ細かくネットワーク環境を再現できます。Xcode → Open Developer Tools → More Developer Tools… とたどるといろんなツールを検索できるページにいくので、そこからダウンロードしてこれます。

プリセットだと LTE っぽいプロファイルがないので、適当にカスタムプロファイルを作ってみました。Downlink/Uplink で別々に設定できることから、Delay はおそらく片道っぽいので、RTT / 2 を設定します。Bandwidth は各社の実効速度データのうち、下限付近を参考に設定しました。

当然ながらテスト環境の回線品質が良くないとテストにならないので、平均とか25%値でテストしようと思うなら無線LANでテストしてたらダメそうです。

また、システム全体に効いてしまうので、他のアプリケーションで通信が発生しまくっているとかなり影響してきます。

throttling しつつ nghttp

なお Chrome の場合は開発者ツールの Network タブで速度と Latency を設定して throttling できます。

ref.

  1. トップ
  2. tech
  3. OS X でのネットワークの throttling を簡単に設定する

h2o は mruby ハンドラで link ヘッダを使って push を指示すると、バックエンドへの問合せと非同期で静的ファイルを push してくれます。

もしバックエンドアプリケーションで link ヘッダを吐いて push する場合、バックエンドアプリケーションの処理が終わったあとから push が始まることになるので、アプリケーションの実行時間分、push できる時間を失うことになります。

server-push の指示をテンプレートに書きたい病

自分はプリロード指示をバックエンド側のテンプレートに書きたい病にかかっており、現状で以下のようなテンプレートコードを書いて、バックエンドから preload ヘッダを吐いています。

r.preload() は link ヘッダを追加するメソッドになっており、これを実際に読みこんでいるHTMLの部分の近くに置くことでリソース管理を簡略化しています。

しかし、これだとバックエンドから h2o へ server-push を指示する形になるので、前述のようにアプリケーションの実行時間分、push できる時間を無駄にします。

バックエンドから server-push するリストをあらかじめ取得する

できれば無駄をなくしたいので、やはり h2o の mruby ハンドラでも link ヘッダを吐くことにします。

ただ、二重に設定を書きたくはないので、バックエンドの吐く link ヘッダを、h2o 起動時に取得しておき、以降のリクエストではそれを元に server-push させるようにします。

まずバックエンドのヘッダをファイルに保存しておきます。ここでは適当に curl を使ってます。

curl -s --head -H 'Cache-Control: no-cache' https://lowreal.net > /srv/www/lowreal.net.link.txt

そして、起動時に read して push する分の link ヘッダを作っておき、ハンドラでそれを送るように設定します。

        mruby.handler: |
          LINK = File.read("/srv/www/lowreal.net.link.txt").
            split(/\r?\n/).select{|l| l.sub!(/^link: /, "") and l.match(/rel=preload/) and !l.match(/nopush/) }.
            join("\n")


          $stderr.puts LINK.inspect


          lambda do |env|
            [ 399, { "link" => LINK }, [] ]
         end

運用上は、適当なタイミング(デプロイタイミング)で curl を打って再度ヘッダを保存して h2o を再起動するようにします。

余談ですが curl を使わずとも mruby ハンドラ内で使える http_request() があるので h2o で完結しそうと思いきや、起動中のコンテキストでは使えないみたいです。

効果

mruby ハンドラで server-push を指示しない場合

mruby ハンドラで server-push を指示する場合

見るべきは、各リソースの responseEnd の時間と、 / に対する requestStart の時間の差です。ただ requestStart はいずれも 1ms 程度なので、注目するのは responseEnd だけで良さそうです。

mruby ハンドラで指示しない場合各リソースの responseEnd は100ms以上になっています。これはバックエンドの処理に約100msほどかかっているからです。一方 mruby ハンドラで指示する場合、responseEnd は 22ms ぐらいになっています。

この例だと、静的ファイルの responseEnd から、バックエンドのレスポンス開始までまだ時間があるので、もっと静的ファイルを server-push する余地がありそうです。

('cache-control: no-cache' をつけているのはバックエンド側のキャッシュを無効にして処理時間を作って差をわかりやすくしているだけです)

  1. トップ
  2. tech
  3. h2o での server-push タイミングの最適化

h2o の casper (cache-aware server-push) を有効にしていると、force reload したときでも push されなくなってしまって、だんだん混乱してきます。YAML を一時的に変えて再起動したりしていたのですが、自分以外にも影響が及ぶのでちょっとなんとかしました。

JS で h2o_casper クッキーを削除してからリロードする

最初に思いつく方法で手軽なやつです。以下のようなブックマークレットでリロードすると cookie がない状態からのリロードになります。

javascript:document.cookie="h2o_casper=; max-age=-1; path=/";location.reload(true);

h2o へパッチをあてて、force reload 時は常に h2o_casper を無視してプッシュさせる

force reload 時にブラウザはリクエストヘッダに Cache-Control: no-cache をつけるので (全てのブラウザかどうかわかりませんが)、その場合にはクッキーを無視して push します (set-cookie は吐かれます)

Cache-Control: no-cache とクライアントが宣言しているなら、casper も無効になっているのは正当ではないか?と思い実装しましたが、ほんとにそうか自信がないので、ひとまず自分のところでテストしています (このサーバには適用済み)。

diff --git a/lib/http2/connection.c b/lib/http2/connection.c
index 4395728..bc83829 100644
--- a/lib/http2/connection.c
+++ b/lib/http2/connection.c
@@ -1185,6 +1185,19 @@ static void push_path(h2o_req_t *src_req, const char *abspath, size_t abspath_le
                 src_stream->pull.casper_state = H2O_HTTP2_STREAM_CASPER_DISABLED;
                 return;
             }
+
+            /* disable casper (always push) when super-reloaded (cache-control is exactly matched to no-cache) */
+            if ( (header_index = h2o_find_header(&src_stream->req.headers, H2O_TOKEN_CACHE_CONTROL, -1)) != -1) {
+                h2o_header_t *header = src_stream->req.headers.entries + header_index;
+                if (h2o_lcstris(header->value.base, header->value.len, H2O_STRLIT("no-cache"))) {
+                    /* casper enabled for this request but ignore cookie */
+                    if (conn->casper == NULL)
+                        h2o_http2_conn_init_casper(conn, src_stream->req.hostconf->http2.casper.capacity_bits);
+                    src_stream->pull.casper_state = H2O_HTTP2_STREAM_CASPER_READY;
+                    break;
+                }
+            }
+
             /* casper enabled for this request */
             if (conn->casper == NULL)
                 h2o_http2_conn_init_casper(conn, src_stream->req.hostconf->http2.casper.capacity_bits);

mruby.handler でなんとかできないかと思いましたが、mruby 側の env に渡ってくる HTTP_COOKIE とかを書きかえても h2o 内部の処理には影響しないみたいなので無理そうでした。

  1. トップ
  2. tech
  3. h2o の casper を一時的に無効にする

すこし昔書いたGoogle Keepのメモを発掘してきました。

NASの維持コスト

電気料金 を30円/kWh、NAS の消費電力を 20W で計算すると、446円/月。24時間動かすので思いのほか電気料金がかかっていることになり、初期コストも含めて考えると、オンラインサービスと案外競合する。

NAS の特徴は

  • 良:データが手元にある安心感
  • 良:LANで完結するので速度が早い
  • 悪:災害によってデータ消失が起こる
  • 悪:自分でメンテする必要があり、故障が起こると思いがけずコストがかかる

オンラインサービスの維持コスト

オンラインサービスの特徴

  • 良:災害があってもデータが失われない
  • 良:自分でメンテナンスする必要がない
  • 悪:大量のデータの取り出しが難しいことがある
  • 悪:ネットワーク経由でしか入出力できないことがあるので速度が遅い
  • 悪:サービスが終了することがある

Google Drive

エンドユーザ向けサービスだが値下げによりかなり安価になっている。
1TBプランが月額$9.99、30Tまではほぼ$0.01/GBとなる。Nearline とほぼ同じだが、容量を使いきっていなくてもプラン容量分課金される点で異なる。
取り出しは当然無料だが速度の保証はない

Dropbox

エンドユーザ向けのサービス。有料プランは1TBのみで、月額1200円。1TB 以上のプランがないので、それ以上になる予測がたつ場合選択肢にあがらない。

Amazon Clould Drive

エンドユーザ向けサービス。有料プランの最大が1TB、1TBのとき年額40000円。月額換算で3333円とかなり高い。

ただし、プライム会員(3900円/年)の場合、写真データ(RAWも含む)が無制限となっており写真だけ上げるなら異常に安い。しかしこの手の無制限サービスはすぐ終了する予測しかない。

Amazon Glacier

約1円/GB
取り出しコストが大変複雑。月あたり使用容量の5%までを1時間あたりに分散してゆっくり取り出せば (約0.0067%/時) 無料。つまり取り出しソフトウェアがちゃんとしていれば、20ヶ月で常に無料で全量を取り出し可能

Google Cloud Storage Nearline

約1円/GB ($0.01/GB)
取り出しも$0.01/GBと書いてあるが、実際はこれに加えて転送量課金がある。$0.12/GB

なので、1TB ダウンロードしようとすると16000円ぐらいかかる。純粋に従量課金なので Glacier より料金はわかりやすいが、無料の範囲というのは存在しないので、ダウンロードでは必ずこの金額必要になる。

エンドユーザ的には価格で Google Drive に対するメリットがない。

  1. トップ
  2. tech
  3. 大量のファイルのバックアップにNASを使うのは適切なのか?

Firefox だと上記のようにリクエストが消滅してタイミングも全てない表示になるみたいです。

Chrome の Network タブだとプッシュしてもキャッシュからひいてくる時間を表示しているのか区別できなくてモヤっとします。

ref. lowreal.net のHTTP2/HTTPS 化を実施 | tech - 氾濫原

  1. トップ
  2. tech
  3. Firefox の開発者ツールのほうが HTTP2 でサーバプッシュされたコンテンツがわかりやすい