9月 252021
 

VarnishのEnterprise版で利用できるパラレルESIがcommunity版でも使えるものをUPLEXが公開してくれました。

ヘビーにESIを利用する人はそこまでいない気もするのですが、これがまたなかなか便利なので紹介します。

ビルド方法

このvmodはVarnishの内部関数に深く依存しているため、ビルド時にはVarnishのコードが必要でビルドに癖があります。

また、動作時にはVarnishとビルドにつかったVarnishのコミットハッシュ値が一致している必要があります。

なお以下の環境で確認しています

OSUbuntu 20.04 LTS
Varnish7.0.0 (454733b82a3279a1603516b4f0a07f8bad4bcd55)
vmod_pesimaster (52cd44e3d8944825d1d84a01c70002a509048c74)

pesiは最新のコードを使えばよいかなと思いますが、もしビルドができない場合はもしブランチで7.0があればそちらを(現時点ではないですが)、もしくはコミットログを眺めて適度に巻き戻るとよいでしょう。

とりあえずVarnishとpesiのコードをダウンロードします。

ubuntu@proxy:~/tmp$ ll
total 16
drwxrwxr-x  4 ubuntu ubuntu 4096 Sep 25 21:38 ./
drwxr-xr-x 12 ubuntu ubuntu 4096 Sep 25 21:37 ../
drwxrwxr-x  3 ubuntu ubuntu 4096 Sep 20 23:33 libvdp-pesi-master/
drwxrwxr-x 15 ubuntu ubuntu 4096 Sep 24 02:18 varnish-cache-master/

一旦こんな感じのパスで説明します。

事前準備

まずはVarnishをautogen.sh / configure / makeしておきます。

ubuntu@proxy:~/tmp$ cd varnish-cache-master/
ubuntu@proxy:~/tmp/varnish-cache-master$ ./autogen.sh
ubuntu@proxy:~/tmp/varnish-cache-master$ ./configure
ubuntu@proxy:~/tmp/varnish-cache-master$ make

なお、make installは不要です。

次にpesiの直下にvarnishのソースツリーにあるm4フォルダをコピーします。

ubuntu@proxy:~/tmp$ cd libvdp-pesi-master/
ubuntu@proxy:~/tmp/libvdp-pesi-master$ cp -rp ../varnish-cache-master/m4 ./

configure.acにあるVarnishのバージョン指定をtrunkからインストールしているVarnishのバージョンに変更します。(今回は7.0.0)

ubuntu@proxy:~/tmp/libvdp-pesi-master$ diff configure.ac ../org/configure.ac
56c56
< VARNISH_PREREQ([7.0.0])
---
> VARNISH_PREREQ([trunk])

次にsrc/Makefile.amに1行追加します。(こちらは今PR投げてるのでもしかしたら直るかもしれないです)

ubuntu@proxy:~/tmp/libvdp-pesi-master$ diff src/Makefile.am ../org/src/Makefile.am
72,73d71
< vmod_pesi_debug.lo: vcc_pesi_debug_if.h
<

とりあえずここまででビルド準備ができました。

ビルド/インストール

ここで注意がいるのはVarnishのソースの場所をフルパスで指定することです。

ubuntu@proxy:~/tmp/libvdp-pesi-master$ ./autogen.sh
ubuntu@proxy:~/tmp/libvdp-pesi-master$ ./configure VARNISHSRC=/home/ubuntu/tmp/varnish-cache-master

ubuntu@proxy:~/tmp/libvdp-pesi-master$ make
ubuntu@proxy:~/tmp/libvdp-pesi-master$ make check
ubuntu@proxy:~/tmp/libvdp-pesi-master$ sudo make install

これでインストール完了しました。

使ってみる

使いかたは簡単で

import pesi;

sub vcl_backend_response {
  set beresp.do_esi=true;
}

sub vcl_deliver{
  pesi.activate();
}

これだけです。(細かい使いかたは README に)

とりあえずこの状態で通常のESIとパラレルESIを比較してみましょう。

$ cat esi1.html
<html>
<body>
        <esi:include src="./time.php?t=1&s=1"/>
        <esi:include src="./time.php?t=2&s=2"/>
        <esi:include src="./time.php?t=3&s=3"/>

</body>
</html>


$ cat time.php
<?php
$s=(int)$_GET['s'];
if($s >10)$s=10;
if($s==0) $s=1;
echo $_GET['t'];
echo "&nbsp;";
echo "start:".date(DateTime::ISO8601);
echo "&nbsp;";
sleep($s);
echo "end:".date(DateTime::ISO8601);
echo "<br>\r\n";

コードはこんな感じで1,2,3秒スリープするPHPをincludeします。

通常のESI

シーケンシャルに処理しているため1+2+3で6秒かかっています。

パラレルESI

パラレルで処理しているため、ベースページ(esi1.html)が読み込まれた後に子ESIのfetchが一気に走っています。

ちなみにESIをネストしても結果は期待通りです。

ESIネスト

<html>
<body>
        <esi:include src="./time.php?t=C%3Cesi%3Ainclude+src%3D%22.%2Ftime.php%3Ft%3D1%26s%3D1%22%2F%3EC%3Cesi%3Ainclude+src%3D%22.%2Ftime.php%3Ft%3D1%26s%3D1%22%2F%3E&s=1"/>
        <esi:include src="./time.php?t=C%3Cesi%3Ainclude+src%3D%22.%2Ftime.php%3Ft%3D2%26s%3D2%22%2F%3EC%3Cesi%3Ainclude+src%3D%22.%2Ftime.php%3Ft%3D2%26s%3D2%22%2F%3E&s=2"/>
        <esi:include src="./time.php?t=C%3Cesi%3Ainclude+src%3D%22.%2Ftime.php%3Ft%3D3%26s%3D3%22%2F%3EC%3Cesi%3Ainclude+src%3D%22.%2Ftime.php%3Ft%3D3%26s%3D3%22%2F%3E&s=3"/>
</body>
</html>

urlエンコードしてる部分はこんな感じで。

C<esi:include src="./time.php?t=1&s=1"/>C<esi:include src="./time.php?t=1&s=1"/>
C<esi:include src="./time.php?t=2&s=2"/>C<esi:include src="./time.php?t=2&s=2"/>
C<esi:include src="./time.php?t=3&s=3"/>C<esi:include src="./time.php?t=3&s=3"/>

要は子ESIでさらに2つincludeしています。イメージは以下の通りです。

+ sleep=1
|+ sleep=1
|+ sleep=1
+ sleep=2
|+ sleep=2
|+ sleep=2
+ sleep=3
|+ sleep=3
|+ sleep=3

通常ESI

パラレルESI

子ESIにおいてもパラレルで動いていることがわかります。

使うときの注意点

きわめて魅力的なパラレルESIですが、注意点があります。

ESIをネストしていて、パラレルESIを有効にした場合は子要素もパラレルESIを有効(pesi.activate)する必要があります。

パラレルESIを有効にしたリクエストにおいて子要素で通常のESIを行うとpanicを起こしてVarnishがrestartします。

ここら辺のことはREADMEに書いてますのでしっかり読んでおいてしっかり検証したほうがいいでしょう。

あ、あとVarnish自体のアップデート記事はそろっと書きます(本書いててサボってたけどもう書き終わってしまったので)


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


    3月 302012
     

    oranieさんが主催しているWebサーバ勉強会という
    全員何かしら発表するという勉強会でESIについて発表してきました。

    ESIについては結構前にqpstudyでもLTしているのですが
    Varnishのバージョンも上がり、いろいろ使いやすくなったので、ここでもう一回とおもいこのテーマで発表しました。
    Varnishの利点はやはりその柔軟な設定(VCL)にあると思うので
    その利点を生かして、単純に静的ファイルをキャッシュさせるだけではなく
    ESIのように複雑なキャッシュをさせるのもいいんじゃないかなと思っています。

    また他の方の発表を聞いてfluentdはやっぱり使いたい!と思った所存です。
    JSONにどうしてもなるのかなぁと思っていたのですがrawで収集できるようにしたよ!という発表もあり既存とも組み合わせしやすくこれはいいなと考えています。
    Apacheについても2.4.1が出た際に僕はEvent MPMばっかに目が言ってたのですが他にも魅力的な機能がありもう一回じっくり見てみようかなと考えています。

    非常に刺激を受けた勉強会でした。
    また機会があれば是非参加したいと考えています。
    参加者の皆様、主催者のoranieさん本当にありがとうございました。


    7月 252010
     

    昨日池袋でインフラ勉強会のキユーピー3分インフラクッキングでESIとインラインCについてちょこっと話してきました。
    LT自体が始めて+飛び入りLTだということでいろいろ自身にツッコミを入れたいところとか多々あるんですが、次回以降の改善点として考えています。
    Continue reading »


    6月 232010
     

    以前書いた記事(VarnishでESIを使うときの注意するべき点)で

    Keep-AliveはOFFにするべき

    ESI使用時はcontent-lengthがブラウザ側に返却されないようなのでOFFはほぼ必須です
    どうしても使いたい場合はESIを使用するときのみkeep-aliveをOFFに・・・

    直Varnishはやめた方がいい

    Keep-aliveをオフにしても特定のクライアント(ab/wgetなど)で接続が維持されて5秒ほど待ってしまうことがあります
    つまりKeep-Aliveが有効かつcontent-lengthがないときの挙動と同じになります
    じゃぁどうすればいいか?
    自分はフロントにNginxを置いてそこからVarnishにproxyしています

    と書きましたがどうやら2.1.X系では改善されたようです

    ということで比較してみます
    Continue reading »


    2月 202010
     

    以下の記事でもチラッと書いたのですがVarnishの現バージョンではESIのTTL指定がesi:includeタグからできません

    VarnishでESIを使うときの注意するべき点

    単純に考えるのであればvcl_fetchでそれぞれのURLで指定すればいいとも言えるのですが
    ESIの利点を多少損なっている感もします
    (個人的にはデザイナ側がこのパーツは10分キャッシュしてもユーザに影響しないよ!とか設定できるのが利点と思ってる)
    じゃぁどうすればいいのでしょうか
    あまりスマートではないですが解決方法があります

    例えばdocument root直下にあるhello.htmlを
    ESIでincludeしたい場合で10秒のTTLを指定する時に

    
    <esi:include src="/esi/t/10s/hello.html"></esi:include>
    
    

    こんな感じの記述でできるようになります
    Continue reading »


    2月 082010
     

    あんまり話題に上らないで個人的に悔しいVarnishのESI(Edge Side Include)ですが
    非常に癖がありますのではまりやすいポイントをとりあえず自分用のメモ程度に

    2010/06/23追記:Ver2.1.2では多少注意事項が変わっていますのでこちらも参照ください
    そもそもESIってなに?

    例えばTwitterのホーム画面で考えてみましょう

    ページは様々な要素で構成されていますが大きく分けて二つに分かれます

    表示される度に更新しなくてはいけないもの(広告・タイムライン)
    表示される度に更新しなくてもいいもの(フォロワーなどの数)

    別にフォロワー数を表示するためにPHPを動かして、DBかmemcacheにアクセスして整形して・・・
    といった処理をしてもいいのですが
    PVが多いWEBサイトの場合、毎回呼ばれる処理はできるだけしたくありません
    PHP→memcacheとしても負荷が結構痛いです
    そういう時にESIがあると、ページの構成要素ごと キャッシュを行い、Varnish側で組み立てて返却してくれます
    PHPやDBまでアクセスがこないので負荷に強くなるとともに、そのキャッシュした構成要素を別の画面で再利用できます

    PC向けの場合Ajaxなどもあるので、一概にESIがすごく有効とも言い辛いのですが(例でTwを上げましたがちょっと悪かったかも)
    携帯向けなどJSが使えない環境で、毎回一枚のHTMLを出力しないといけない場合非常に効果的です
    また転送速度が上がるのでPVが上がるかも しれませんしSEOにも効果がありそうです
    (Googleはどれぐらいでページが表示されるかをSEOの評価としてるみたいです)

    Continue reading »