1月 192015
 

このブログを見てる人だとご存知だとは思うのですが、Varnishはいろんな機能があるリバースプロキシです。
VCL、ヘルスチェック、強力なログ機能、そしてESIなどの機能が存在します。
ESI以外の記事は偶に見かけるのですがESIはあまりみないなーというのと


こんな乗りで去年の10月あたりから知り合いのサイト(一般的には大規模にあたるぐらいのPV)にESIを入れたので
(特定できてもそっとしておいてください)
その時に効果や注意したことをメモ的に残そうと思います。

まずESIって何かというとESIタグをページ中に挿入することでVarnish側でそのURLの内容で置換してくれる技術です。
詳しくは以下のスライドを参照してください、Ver3時の資料ですが大幅には変わっていません。

最初にそのサイトでのESIの効果を説明します。
ESIを入れる動機は主に速度改善と負荷軽減だと思いますが、非常に良い結果がでました。
esi02
導入自体は10月中~末あたりから始めたのですがそれ以前は割りとグラフが上下していたのが入れ始めてからガクッと落ちて安定的になっているのがわかると思います。
esi03
esi04
サーバ応答時間とページダウンロード時間のグラフは非常に面白い変化をしています。
応答時間はESIで改善し、逆にダウンロード時間は悪化しています。
これの理由は単純で、ESIを行っていない時はページの内容を全部準備してからresponseしていたところが、
ESIによって内容があるところまでVarnishがresponseし、無ければ取りに行く(そこでwait)ようになったからです。
わかりづらいのでコードを交えて解説します


<html>
<head>
<link rel='stylesheet' id='dashicons-css'  href='/css/hoge.css' type='text/css' media='all' />
<script type='text/javascript' src='/js/mage.js'></script>
</head>
<body>
...
<esi:include src="slow.php">
...
</body>
</html>

上記の場合だとesi:includeで指定したslow.phpを読むようになっています。
仮にslow.phpが3秒かかったとしても、ESIタグの前まではVarnishがレスポンスします。
そのため応答時間は速くなり、ダウンロード時間が遅くなりました。
ここで一つポイントなのが、ESIタグまで先にレスポンスできることです。
大体headにcssやjs読み込みがあるのでブラウザは先にそれらリソースを読み込み、更に途中までレンダリングすることが出来るため表示速度の改善も期待できます。
仮にページの表示に同じ時間かかるとしても、最初の1秒ぐらいでなんとなくコンテンツが出始めるのと、3秒ぐらいたってから初めて出るのだと前者のほうがよいと感じると思います。(少なくとも僕はそうです)

esi01
これはとあるページの情報ですが、やはり速くなっているのがわかります。
僕の場合は、適用前2~3秒ぐらいだったのが今さっき見たら0.2秒を切っていました。

そしてサーバコストも全体で30%程度の減、ESIだけだと内5~10%程度の寄与があったのではと考えています。
(並行で幾つかの改善を行ったため全部がESIの効果ではないです。。)
他にもツールによる大量のダウンロードに対する負荷耐性もよくなりました。

このように非常に効果が高いESIですが、銀の弾丸なんてどんなものにもあるわけではなく非常に注意が必要です。
例えば以下のケースを考えてみましょう

ヒットレートが低く子要素を多く含む
現在のVarnishでは子要素の取得を逐次処理で行うため、例えば子要素が10ありそれぞれで0.5秒かかりヒットレートが50%だとすると平均で子要素の取得だけで2.5秒かかります。
つまり速度改善を目論んで入れたのに逆に遅くなってしまうケースもあるということです。
ちなみに現在開発中のVarnish4.1には子要素を並列で取得する改善(Parallel ESI)が行われる可能性があるため(wishlist)将来は改善する可能性があります。

ヒットレートが低く使用しているフレームワーク等のオーバーヘッドが大きい場合
1ページをレスポンスするために複数のページを使用するため当然ながらリクエスト数が増えます。
そのためフレームワーク等のオーバーヘッドが大きい場合は当然負荷が上る可能性があります。

じゃぁヒットレートが低いとそもそもアウトなのかというとそうでもなく
ベースページはヘッダとフッタのみが記述して、実コンテンツを1つのESIで読み込む。(そのままラップする感じ)
こうすることでヒットレートが低くてもCSSやJSなどが含まれるヘッダ部分は先にブラウザにレスポンスできるためレンダリング速度の改善が見込めます。

さて、上記のような注意事項はそもそもESIを適用するかしないかの判断になるので今回は置いておいて
今回は実際にESIを行う上でこうやっておくと楽だったり、意識しないとハマる可能性があるポイントを紹介します。

  • キャッシュのスコープ管理
  • キャッシュするステータスコードを意識する
  • 直アクセスの禁止
  • ストレージについて
  • set-cookieの扱い
  • ログについて
  • 1つずつ解説します。

    キャッシュのスコープ管理
    どんなキャッシュを行う上でも一番重要なことは何をキーにキャッシュするかです。
    例えば、Aさん向けのキャッシュがBさんに表示されてしまう事は絶対に避けないといけないことです。
    このことを防ぐために例えばユーザの情報を含むものはキャッシュしないというのも手です。
    しかしそのようなデータ(常に表示されるようなようこそ○○さんみたいなバーとか)もキャッシュしたいものです。
    わかりやすくミスっても漏洩などの最悪のパタンにならないようにと考えて僕はパスで管理するようにしています。

    /esi/[ルール]/foo/…/bar.php

    このルールはVaryみたいなものです。
    例えばuserと入ってればuser毎にキャッシュするみたいな感じです。
    あくまで例ですが以下のような感じです。

    /esi/user/ ユーザー毎にキャッシュ、非ログインユーザはキャッシュしない
    /esi/browser/ UA判定を行いPC/SPで別々のキャッシュを持つ
    /esi/その他(common等)/ 特に判定せずに共通のキャッシュを持つ

    他にも毎時0分にキャッシュがクリアされるようなルールがあっても良いと思います。(TTLの管理をテンプレート/アプリのどちらでやるかという話もありますが)

    実際にそのサイトで使ってるVCLは流石に出せないのでサンプルでVCLを書いてみました。
    動作確認はしていないのでこんな感じと考えてもらえれば嬉しいです。

    
    sub vcl_recv{
      unset req.http.x-varnish-hash;
      ...
      if(req.url ~"^/esi/"){
        if(req.url ~"^/esi/user/"){
          if(req.http.cookie && req.http.cookie~"(.* )?user="){
            set req.http.x-varnish-hash = regsub(req.http.cookie, "^(.* )?(user=[^;]+)(;.*)?$","\2");
          }else{
            unset req.http.cookie;
            return(pass);
          }
        }else{
          unset req.http.cookie;
          if(req.url ~"^/esi/browser/"){
            if(req.http.user-agent ~"(iPhone|iPad|iPod|Android)"){
              set req.http.x-varnish-hash = "sp";
            }else{
              set req.http.x-varnish-hash = "pc";
            }
          }
          return(hash);
        }
      }
      ...
    }
    sub vcl_backend_response {
      ...
      set beresp.do_esi = true;
      ...
    }
    sub vcl_hash {
      if(req.http.x-varnish-hash){
        hash_data(req.http.x-varnish-hash);
      }
    }
    
    

    ここで重要なのは/user/以外の場合はクッキー等ユーザーの識別情報を削除することです。
    こうすることで万が一/common/にユーザーごとの出し分けが必要な物が混じってもユーザーを識別できないので重大な事故には至りません。
    またvcl_hashで追加しているのがreq.http.x-varnish-hashだけでreq.url等が含まれていない理由はここを参照してください(同じVCLアクション(vcl_recvなど)を複数定義する)

    パスで分けるのはそのパスにコントローラーを放り込んでおけばその通りにルールが適用される気軽さです。
    わざわざコード側でこのコントローラーはuserのキャッシュを行ってうんぬん・・・と書いてレスポンスヘッダなどでVarnish側にどう動かすか渡してもいいんですが
    テンプレートから見てこれどんな動きするっけ?みたいにわからなくなりがちなのでこうしています。
    ESIのパフォーマンス以外の良い所は部品を組み合わせるように要素を自由に配置してページを作れるところだと僕は考えています。
    いわばテンプレートが主役といった感じです。

    キャッシュするステータスコードを意識する
    ページのレスポンスコードは当然ながらベースページに依存します。
    子要素が503を返そうが302を返そうがベースページが200を返していればクライアントでは200で見えます。
    そこでキャッシュすべきステータスコードを注意深く意識する必要があります。
    50Xをキャッシュしないのは当然として、302なんてものもキャッシュしないほうがよいでしょう、というか200だけキャッシュするぐらいに絞ったほうが良いです。

    直アクセスの禁止
    /esi/以下に直アクセスされても弾く場合はreq.esi_levelを利用します。

    
    sub vcl_recv{
      if(req.esi_level == 0 && req.url ~"^/esi/"){
        return(synth(403));
      }
    ...
    }
    
    

    req.esi_levelはリクエストがどれだけネストしているかを示します。
    0はベースページで1以上は子要素になります。(2以上は子要素からincludeされた要素)
    ちなみにネストはデフォルトでは5になっており、これ以上ネストしたい場合はパラメータのmax_esi_depthを変更する必要があります。

    ストレージについて
    ESIのストレージはmallocやSSD/PCIeSSDを使った高速なfileストレージに入れるのをおすすめします。
    HDDなfileストレージに入れるのは余程の理由があるかつチューニングに自信がない限り辞めたほうがよいです。
    これは遅延が直接ページのレスポンス速度に関係するからです。
    ページのレスポンスが遅れれば、その遅れた部分に含まれてるimgタグ等による読み込みが遅れて全体のレンダリング速度に影響してきます。

    また、同一インスタンスで画像などの大きめな静的ファイルも配信している場合はESI専用のストレージ定義を作るのも有効です。
    VarnishのストレージはPersistentを除いてLRUで管理しているのでTTL前に容量不足でキャッシュがnukeしても問題ないかと思うこともあると思いますが、それだとgraceが効かなくなってしまいます。
    なぜならいくらキャッシュヒットしていて優先度が高いオブジェクトでも容量が足りない状態でexpireしたら当然すぐに回収されるからです。
    graceの動きはこの記事を参考にしてほしいですが
    簡単にいうとexpireしてgrace期間中にアクセスがあった場合は期限切れオブジェクトをレスポンスしてバックグラウンドでフェッチすることでexpireしても高速にレスポンスできます。

    今回のサイトでは3種類のストレージに分けました。

  • ほぼすべてのページで利用され、nukeすると多くのページで速度が落ちるようなもの
  • 自由文で入力が出来るなど総数がわからなく、ヒットするものそうでないものの差が極端なもの
  • 上記以外のもの
  • 分けることで例えば自由文のものに押し出されてgraceが効かなくなるということもなくなります。

    ちなみにストレージを分けるにはberesp.storage_hintを使います。
    起動パラメータ

    
    -s esi_memory=malloc,1G -s esi_file=file,/var/lib/varnish/varnish_storage_esi.bin,7G -s default=file,/var/lib/varnish/varnish_storage_default.bin,7G
    
    

    VCL

    
    sub vcl_backend_response {
      set beresp.storage_hint = "default";
      ...
      if(req.url ~"^/esi/"){
        if(req.url ~"^/esi/xxxxx/"){
          set beresp.storage_hint = "esi_memory";
        }elsif(req.url ~"^/esi/yyyy/"){
          set beresp.storage_hint = "esi_file";
        }
      }
    }
    
    

    起動パラメータの-s esi_memory=malloc,1Gとberesp.storage_hintで指定する値と一致しているのがわかります。

    set-cookieの扱い
    子要素でset-cookieを発行しても当然ながらブラウザ側には届かないので注意が必要です。
    ESIを使う場合はベースページでのみset-cookieを発行して子要素では決して発行しないようにしてください。

    ログについて
    キャッシュを行うため当然ながらws側にはすべてのログがいきません。
    完全なログを取る必要がある場合は当然varnish側で取得する必要がありますが
    割り切りでベースページはキャッシュしないとするだけでもだいぶ楽になると思います。

    まとめ
    ESIは結構楽しいので使ってみようと考えてみる一助になれば幸いです。
    あと、当然ですがすでにあるサイトに全適用するのは割と辛いので効果高いところをつまみ食いしてみてどうなるか見てみるのも面白いと思います


     Leave a Reply

    You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

    (required)

    (required)