2月 032016
 

Varnish4.1.1がリリースされました。
ドキュメントの修正や、バグフィックスが主ですが幾つかの仕様変更があります。
4.1.0を利用している場合はアップデートをおすすめします。

ダウンロードはこちら
changes

機能追加

varnishncsaに-fオプションが追加されました
これは単純に-Fで指定するフォーマットをファイルから読み込みができるというだけです。
エスケープ等が面倒な時はよいかもしれないです。

仕様変更

varnishncsaはデーモンで動作する際に-wオプションが必須になりました
-wはファイルに出力するオプションで、通常デーモンで動作させる場合は指定しているものなので特に影響はないかと思います。

If-None-Match(INM)とIf-Modified-Since(IMS)リクエストが同時に来た場合IMSを無視します
この動作自体はRFC7232の3.3に記述されている通りです
(A recipient MUST ignore If-Modified-Since if the request contains an If-None-Match header field)

ステータスコードが1xx, 204, 304の場合はContent-Lengthヘッダをレスポンスしません
これらのステータスコードはbodyを含みませんので不要です。

VCLをネストしてincludeする際のカレントディレクトリをそれぞれのVCLのディレクトリに変更しました
いまいち一文で書くとわかりづらいのですが用はこういうことです。


/etc/varnish/default.vcl
/etc/varnish/common/base.vcl
/etc/varnish/common/acl/internal.vcl
/etc/varnish/common/acl/admin.vcl

例えばVCLをこのように配置していて相対パスでincludeする場合


~4.1.0
■/etc/varnish/default.vcl
vcl 4.0;
include "./common/base.vcl";

■/etc/varnish/common/base.vcl
vcl 4.0;
include "./common/acl/internal.vcl";
include "./common/acl/admin.vcl";

4.1.0までは基点となる/etc/varnish/を基準とした相対パスで書く必要がありましたが、
4.1.1からは


4.1.1~
■/etc/varnish/default.vcl
vcl 4.0;
include "./common/base.vcl";

■/etc/varnish/common/base.vcl
vcl 4.0;
include "./acl/internal.vcl";
include "./acl/admin.vcl";

このようにincludeが記述されたVCLのディレクトリを基準として読み込みます。
割と便利だと思います

hit-for-passもgrace動作するようになりました(#1818)

vcl_dir, vmod_dirにコロン区切りで複数のディレクトリを指定できます

varnishreplayが削除されました
少し前からコンパイルされないようになっていたのですがコード自体も削除されました。

バグ修正(一部抜粋)

大きなファイルを扱う際に遅くなっていたのを修正(#1798)

IPv6アドレスのパースに失敗するのを修正(#1801)

PROXYプロトコルを利用した際にvarnishlogが正しく表示されないケースがあったのを修正しました(#1804)
Proxyプロトコルのvxidが0になっていてその影響で-g request, sessionがうまく動きませんでした。

INMリクエストでETagを比較する際に弱い比較関数を使用するようにしました(#1816)
RFC7232に準拠した形です。

vsl-queryでフィールドをfloatsで評価しようとした場合に評価できないのを修正(#1845)
4.1.0からのデグレですが、具体的には”timestamp:resp[2] > 1.”のような絞り込みができなくなってました。


12月 052015
 

この記事はVarnish Cache Advent Calendar 2015の4日目の記事になります。

VCLには様々な変数の型がありまして、それぞれの型の間では暗黙的に変換されるものと、vmod_stdを使用して明示的に変換するものがあります。
型が結構多いのでいまいち何を使うか忘れることが多いので図にしてみました。
types
なお、ここでは特に変換にかかわらない型については取り上げていません(BLOB,ENUM,HTTP,PRIV_CALL,PRIV_VCL,PRIV_TASK,PRIV_TOP,PROBE,VOID)

ちなみに各型から暗黙的にSTRINGへ変換すると以下のようになります。

変換例
BYTES 2.000
DURATION 2.000
REAL 2.000
BOOL false
BACKEND default
INT 1
IP 192.168.1.1
TIME Fri, 04 Dec 2015 18:31:12 GMT
HEADER example.net

また、STRING_LISTですが基本的にはSTRINGと特に違いはないのですが、
これを引数として受け付ける関数の場合は型変換を意識しないとハマります。
例えばstd.logというログを出力する関数があるのですが、これは引数がSTRING_LISTです。
それを意識して以下のようなVCLを書くとエラーになります。


std.log(std.duration("10w",0s));


Command failed with error code 106
Message from VCC-compiler:
Wrong argument type.  Expected STRING_LIST.  Got DURATION.
('input' Line 31 Pos 31)
std.log(std.duration("10w",0s));
------------------------------#-
('input' Line 31 Pos 1) -- ('input' Line 31 Pos 30)
std.log(std.duration("10w",0s));
##############################--
Running VCC-compiler failed, exited with 2

これはdurationから一気にSTRING_LISTに暗黙的に変換できないからです。
そこでどうやるかというと、一度STRINGを経由させてあげればよいので


std.log("" + std.duration("10w",0s));

このように空文字を結合すると良いです。

まとめ
変数がどのように型変換が出きるかを知っておくと、その型では出来ない演算を変換した先でやって戻すみたいなことが出来ます。
覚えておくと結構便利なので、頭の片隅に置いておくと良いかなと思います。


12月 022015
 

この記事はセカイエ Advent Calendar 2015Varnish Cache Advent Calendar 2015の2日目の記事になります。
かなり前の話(8月)になるのですがリノコという定額リフォームサービスのサイトがTV東京のワールドビジネスサテライト(以下WBS)に取り上げられました
ありがたいことにたっぷり取り上げていただきました。その際に少しだけVarnishを使って負荷対策をお手伝いしたのでその時のことを書きます。
(なおここでとった対策は緊急ということで外しています。)

TV放映される上で負荷対策として検討したこと

  1. スケールアップ・アウト
  2. トップページ及びそこから回遊されるページの静的化
  3. Varnish導入

動き的には1を検討してみてその結果たりなさそうという判断を行い2,3を並列で行った感じです。

スケールアウト
リノコではクラウドを利用しているわけで当然スケールアウトを考えるわけですが
残念がらコード側があまりスケールするような仕組みになっておらず、例えば10倍サーバを投入してもスケールアップ・アウト出来ない箇所がボトルネックになって無駄になってしまう状態でした。
そのため最低限のスケールアップ・アウトを行いました 。

トップページ及びそこから回遊されるページの静的化
現地メンバーにお願いしましてコードを修正することでページの静的化することで、負荷に耐えるように修正を加えようとしました。
このアプローチ自体は間違っているとは考えていませんが残念がら切り戻しています。
単純にバグってしまったということです。敗因は明らかで単純に時間がなかったということにつきます。
今回のTV放送を知ったのが放映前日で、この作業をはじめたのが放映6時間ぐらい前だったからです。(そして切り戻し判断は放映2時間ぐらい前です)

Varnish導入
リノコのサイトでは幸いな事にキャッシュを行うことができる・出来ないの判断が比較的分かりやすかったため、試しに設定を書いてみて投入してみました。
この試みは割とうまく行ったためそのままTV放送を迎えました。

結果について
まずはどの程度のリクエストが来たかですが
image2015-11-29 16-43-13
割とビビるリクエストが着ているのがわかります。
でサイトが落ちたかどうかということなのですが半分落ちました。
この半分というのはVarnishでキャッシュが可能でキャッシュが出来たページについては特に問題なくレスポンスが出来て
キャッシュが不可能なページやキャッシュは可能だが深い階層で未キャッシュのページは落ちた感じです。
負荷対策を行う上で重要なのは、かけることが出きるコストを正確に把握し、何処の優先度を上げて何処を下げるかだと思っています。
特に今回のケースでは対策に費やされる時間がほぼなかったため、最小の対策で最大の成果を得る必要がありました。
今回はTV放映時にサイトを全部落とさない、特にLPとそこから回遊しそうなページについて落とさないを目標としました。
そういう意味では今回の対策はうまく行ったと考えています。

そもそもなぜVarnishを使ったか
どうせお前がVarnish好きだからじゃないかという指摘もあるかもしれませんが
今回のケースではVarnishを使う以外の選択肢はありませんでした。
もう少し時間があれば他の手段も取れましたがその場合でも良い選択肢だと確信しています。
『幸いな事にキャッシュを行うことができる・出来ないの判断が比較的分かりやすかった』
と書きましたがこのルールは以下でした

  • 特定のパス以下のURLはキャッシュ不可
  • クッキー中に特定キーが入っていた場合はキャッシュ不可
  • 同一URLでUAによってコンテンツの出し分けを行っている(PC/スマホ)
  • etc

例えば1URLで最大どの程度のパタンがあるかというと

  • キャッシュ可能+PC
  • キャッシュ可能+SP
  • キャッシュ不可+PC
  • キャッシュ不可+SP

と4パタンがありました。
まさにVarnishの設定言語であるVCLの得意とすることで、設定は47行でした。(実質コードですが)
しかもこの対策を行う上でサイト側のコードは修正しておりません。
このような場合において最低限の対策で効果を得るにはVarnishは最適だと考えています。

まとめ(負荷対策の重要性)
メディアに取り上げられるというのはいわばボーナスステージだと僕は考えています。
しかし、見に来ていただいた方が実際に興味を覚えてもらうためにはまずサイトを見れる状態を作らなければなりません。
普通に考えて初めてサイトを見に行って重くて見れなかった、そもそも見れなかったとかであれば、すぐに記憶から薄れ再訪することはないでしょう。
そのためサイトをなるだけ見れる状態にするのを考えるべきでしょう。

また、負荷のかかり方も様々です。

  • 事前にわかるかどうか
    • 事前連絡がある
    • 事前連絡がない(予測不能)
    • イベントなどでそもそもわかる(エイプリルフールやセール等)
  • 負荷の上がり方
    • 一瞬で上がってその後落ち着く(TV)
    • 一気に上がりつつその後もじわじわ上がる(ネットメディアなど)

どんなパタンがあるだろうとさくっと書いてみましたが(サイトの特性によってここは変わると思います)それぞれの組み合わせでどのような対策を行うか考えてみるのも良いと思います。
例えば事前連絡があるのであれば、インスタンスをもりもり多めにあげて(AWSであれば)ELBの暖気申請をして・・・みたいな対策も打てます。
逆に事前連絡がないのであれば、オートスケールするまでの時間までを如何に稼ぐかという話になってくるかもしれません。
更に一瞬で上がっていつ起こるか予測不能であればその場合でもどのコンポーネントを落とさないようにするかなどの事前の設計・対策が重要でしょう。
もちろん、どのパタンにおいてもスケールするようにコード修正するとかクラウドの機能を使う/業者の選定も大事です。
また、どのパタンでもキャッシュ可能なコンテンツをキャッシュすることは有効な手段の一つだと思います(Varnishでもなんでもいいですが)


12月 012015
 

この記事はVarnish Cache Advent Calendar 2015の1日目の記事になります。

何回か散発的に取り上げたことはあるのですがVSL-Queryの使い方についてまとまっては書いていなかったので書いてみます。
varnishlogのフォーマットの説明についてはv3の頃の記事ですがVarnishのログにアクセスしてみよう!を参考にしてください。

Varnishは他のミドルウェアでよくあるようにアクセスログをファイルに直接書き込むということはせずにリングバッファな共有メモリ(VSM)に出力します。
その際に出力されるログ(VSL)は非常に多岐に渡ります

  • ヘッダ(クライアントからのリクエストヘッダやバックエンドからのレスポンスヘッダなど)
  • ヘッダへの操作
  • どのアクション(vcl_recvなど)が呼ばれて何をリターン(hashなど)したか
  • std.logで出力した任意のログ
  • などなど

標準ではoffにされていますがonにすることでVCL中のどこのブロックを通過したかもトレースすることも可能です。
varnishncsaやvarnishlogなどの各種ツールはこのログを読んで出力します。
vsl
もちろんそのまま使うことも多いのですが、例えばエラーがでた・遅いリクエストだけを抽出したいということがあります。
その際に利用するのがVSL-Queryというもので、要はVSLに対して絞り込みを行う式を作成できます。


varnishncsa  -q "respstatus >= 400"

例えばこうすることでステータスコードが400以上のものを絞り込んで出力することができます。
今回はVSL-Query全般についての記事を書こうと思います。

VSL-Query

VSL-Queryは3つの要素からできています。

  • ログのグループ化
  • トランザクションの階層化
  • クエリ評価する
ログのグループ化

グループの種類は4種類あります。

raw
ログ1行を表します(何もグループ化されていない状態)
rawの場合はReqURLなどトランザクションに関わるデータとヘルスチェック等の非トランザクションデータの両方が含まれています。
ログを見た際にvxidが0のものは非トランザクションデータです。

vxid
複数行のログをHTTPトランザクション毎にグループ化します。
例えばクライアントからのトランザクションを処理する際にバックエンドまで問い合わせを行った場合は
クライアント<->VarnishとVarnish<->バックエンドの2トランザクションに分かれます。
なお、このデータにはraw時に含まれていたトランザクションにかかわらないデータ(ヘルスチェックなど)は含まれていません。
ちなみにvxidとはVarnish Transaction IDの略でトランザクションを識別するためのIDです。
X-Varnishヘッダで挿入される数値についてもそれです。

request
複数のHTTPトランザクションをまとめてリクエスト毎にします。
ESIやrestartを利用した場合はrequest内にrequestが含まれる事があります。
基本的にこの粒度で見ていればクライアント・Varnish・バックエンドでの流れがわかるので便利です。

session
複数のリクエストをまとめてセッション毎にします。
複数のリクエストが含まれるケースはkeep-aliveが有効な場合です。

grp
図にするとこんな感じです。

トランザクションの階層化

vsl-queryではログのグループ化を行うと同時に階層構造を作成します。
例えばVarnishからバックエンドへのトランザクションはその前にクライアントからVarnishへのトランザクションがあります。当然ですがそれは親子関係です。
トランザクション間の親子関係を作っているので、当然ながらログ一行ずつをグループとしているrawやトランザクション単体をグループとしているvxidでは階層化は行われません。
requestとsessionにおいて階層化が行われます。
実際に例で考えてみましょう。

グループ分けをrequestでVarnishがクライアントのリクエストを受付して、バックエンドに問い合わせして返却するケースでの階層は以下になります。


[Lv1] +ClientとVarnishのやりとり
[Lv2] | +VarnishとBackendのやり取り

次にグループ分けをsessionにしてそのセッションからのリクエストが2つあった場合は以下になります。


[Lv1] +セッション
[Lv2] | +ClientとVarnishのやりとり
[Lv3] | | +VarnishとBackendのやり取り
[Lv2] | +ClientとVarnishのやりとり
[Lv3] | | +VarnishとBackendのやり取り

実際のログだとどのように出力されるかというと(グループ分け=request)


*   << Request  >> 44645623
-   Begin          req 44645581 rxreq
-   Timestamp      Start: 1448894183.500204 0.000000 0.000000
-   Timestamp      Req: 1448894183.500204 0.000000 0.000000
-   ReqStart       ****** 47457
-   ReqMethod      GET
...
-   VCL_return     fetch
-   Link           bereq 44645624 pass
-   Timestamp      Fetch: 1448894183.708612 0.208408 0.208408
-   RespProtocol   HTTP/1.1
...
-   Timestamp      Resp: 1448894183.710162 0.209958 0.001442
-   ReqAcct        881 0 881 1994 7476 9470
-   End
**  << BeReq    >> 44645624
--  Begin          bereq 44645623 pass
--  Timestamp      Start: 1448894183.500314 0.000000 0.000000
--  BereqMethod    GET
-

こうなります。
なおレベルは1スタートとなります。

クエリ評価

グループとクエリの関係
何に対してクエリ評価を行うかというと、先ほどグループ化した中でqueryを評価して、一致するものがあればそのグループを出力します。
つまりグループがrawの場合は1行単位で評価するので


# varnishlog -graw -q "respstatus >= 400"
  61146578 RespStatus     c 404
  35190583 RespStatus     c 404
  52614757 RespStatus     c 404
  52656165 RespStatus     c 400

条件にひっかかる行だけが出力されます。

逆にsessionで評価した場合


# varnishlog -gsession -q "respstatus >= 400"
*   << Session  >> 60621569
-   Begin          sess 0 HTTP/1
-   SessOpen       ******* 62077 :80 ******* 80 1448897491.204370 99
-   Link           req 60621570 rxreq
-   Link           req 60621571 rxreq
-   VSL            store overflow
-   End            synth
**  << Request  >> 60621570
--  Begin          req 60621569 rxreq
...
--  RespStatus     200
--  RespReason     OK
...
--  End
**  << Request  >> 60621571
...
--  RespStatus     404
--  RespReason     Not Found
...

同一セッション内で404を返しているリクエストがあるためステータス200で返してるリクエストも引っかかってきています。
たとえば400以上のステータスコードを吐いてるリクエストのみを取得するのであればグループはrequest若しくはvxidが適切でしょう。
このようにグループ分けは狙ったログを出力する際には非常に重要です。

クエリの文法
クエリは次のように書きます。
[レコード] [演算子] [被演算子]
例: respstatus >= 400

レコードは次のように書きます。
{階層レベル}タグリスト:レコードのPrefix[フィールド]
これは全て指定する必要はなく、最低限タグだけを指定すればOKです。
フルで指定するとこのような指定になります
{2+}Timestamp:Resp[2]
1つずつ説明します。

階層レベル ( {2+}Timestamp:Resp[2] )
先ほど説明したトランザクションの階層化におけるレベルになります。
レベルは次のような指定の仕方があります。

指定方法/サンプル 説明
{2} Lv == 2
{2+} Lv >= 2
{2-} Lv <= 2

ちなみに{0+}も指定できます。この場合は全てのレベルと一致します

タグリスト ( {2+}Timestamp:Resp[2] )
タグの一覧はこちらになります
また、複数のタグを以下のように指定できます

指定方法 サンプル 説明
[tag],[tag] respheader,reqheader カンマ区切りでタグをを完全一致で指定します。
*[tag名の一部] *header 後方一致するタグを指定します。
[tag名の一部]* resp* 前方一致するタグを指定します。

[,]と[*]は同時に指定可能です。
また、[*]を途中で挿入することもできません。


#OKパタン
req*,resp*
#NGパタン
*eq*

レコードのPrefix ( {2+}Timestamp:Resp[2] )
データが「:」で区切られている場合はそれをキーとして使えます。
例えばTimestampでのイベントラベルやReqHeaderなどのヘッダ名に対して使用できます。
例:ReqHeader:User-Agent #リクエストヘッダのUser-Agentを指定

フィールド ( {2+}Timestamp:Resp[2] )
データがスペースで区切られている場合はそれを区切り文字として1スタートで指定できます。
例えばレスポンスが完了するまでにかかった時間を指定したい場合は以下のようにします。
例:Timestamp:Resp[2] #Timestamp:Respの第2フィールドを指定

演算子
演算子は以下のものが使用できます

タイプ 演算子
数値(int,float) == != < <= > >=
文字列 eq ne
正規表現 ~ !~

被演算子
以下が使用できます

タイプ サンプル
int 100
float 0. 0.8
string hoge
regex (iPad|iPhone)

なおregexはpcre(3)を利用しているので、例えば大小文字を無視する場合は以下のようにします


(?i)wget

文字列や正規表現は基本的に[‘]か[“]でquoteするのが良いかと思いますが、以下の文字のみで構成する場合はquoteは不要です


a-z A-Z 0-9 + - _ . *

論理演算子
先ほどの[レコード] [演算子] [被演算子]を一つの式として、論理演算子と組み合わせることで複数の条件を指定できます。
使用可能なのは以下です。

指定方法 説明
not [expr] exprが一致しない場合に真
[expr1] and [expr2] expr1と2が両方とも真の場合に真
[expr1] or [expr2] expr1か2のどちらかが真の場合に真

もちろん[expr1] and not [expr2]や[expr1] or not [expr2]の指定も可能です。

その他
他には以下の様な指定が可能です。

指定方法/サンプル 説明
respstatus タグ名だけを指定するとそのタグが存在する場合に真となる
(fetcherror ~’no backend’ and respstatus == 503) or respstatus == 400 ()を式中に含めることが可能です

実際のサンプル

自分がよく使うqueryの一部を紹介します

vcl_log:[key] ~ 任意の文字列
std.log(“key:文字列”)でデバッグ用の文字列を仕込んで、それでマッチさせます。
場合によっては数値を利用して比較演算子と組み合わせるのも便利です。

reqheader:Host ~ [ホスト名]
ホスト名を絞って表示したい場合

respstatus >=400
エラーを起こしたもの

最後に

これ書いてる時にバグっぽいのを見つけたので調べてバグならチケット切ってきます(その部分の記述は削りましたw)


10月 122015
 

Varnish4.1.0がリリースされました。
多くの新機能・改善・バグフィックスとほんの少しのVCLの変更があります。
特にバグフィックスを求めるユーザにとっては朗報です。
今までチケット上は修正されたけど各ブランチにマージされていなかったものが全て今回4.1ブランチにマージされたのでそれを求めるユーザにとっても更新するのも良いと思います。
割と大規模な変更があるので安定性について気になっている方もいると思いますが、4.1.0-beta1を2週間強ほど知り合いのサイト(ESI含む)に入れて運用しましたが
踏んだバグは一つ(#1792)で修正済みで安定して動いています。
また、CPUの使用率も下がっているようです。
ダウンロードはこちら

VCLの変更について
本家のアップデートガイドでも書いてある通り4.0.xのVCLをそのまま使用することが出来ます。
但しdeprecated的なのが増えているのとvmod_stdで引数の追加があるので注意が必要です。

HTTP/0.9のサポートを終了しました
むしろしていたのかという気持ちに・・

PROXY Protocol(v1/v2)のサポート
PROXY Protocol(v1/v2)をサポートしています。
使用する場合は起動オプションで以下のように指定します。(,PROXYを追加)


-a [IP Address]:[Port],PROXY

これに伴いremote.ip/local.ipが追加されました(後述)
接続してくる上位PROXYサーバのIPを制限したい時(ACL)に便利です。
またVarnishではhitchというPROXY Protocolを喋るTLS/SSLProxyを作っており(この前バージョンが1.0.0になった)
これと組み合わせるのを想定していると考えています。

VCLの自動cold/warm機能
Varnishはvclをreloadをした場合でも古いvclを保持しておいて後で使用することが出来ました。
この状態ではオリジンにヘルスチェックが飛んでしまい、それを防ぐには古いvclを削除するには明示的にdiscradをする必要がありました。
しかし4.1では使用されなくなったVCLは一定時間経過後(vcl_cooldown)にcold状態になりヘルスチェックが止まります。

セキュリティの強化(jails)
4.0.3


root     30262  0.0  4.2 124540 84320 ?        SLs  Oct05   0:43 /usr/sbin/varnishd ...
nobody   30264  0.1  4.2 582932 85724 ?        Sl   Oct05  15:19  \_ /usr/sbin/varnishd ...

4.1.0


varnish   1278  0.2  0.2 124768  5376 ?        Ss   16:32   0:00 /usr/sbin/varnishd ...
varnish   1280  1.7  5.8 1639192 117276 ?      Sl   16:32   0:00  \_ /usr/sbin/varnishd ...

今まではマスタープロセスはroot権限で動いていたのですが4.1からはセキュリティの強化のためになるだけ別ユーザで動くようにしました。
デフォルトではvarnishを使います。
もし任意のユーザ名を指定したい場合は起動オプションで-jオプションで指定可能です。

VMODでアクセスできる範囲の大幅な強化
これはどちらかと言うとVMODを作る人向けなのですが
以下のことが可能になっています

  • レスポンスボディに対するフィルタ機能(VFP/VDP)
  • バックエンドの動的生成
  • カスタムバックエンドの作成

まだすべて試しきれていないのですがどれも非常に強力です。
これは機会があれば別記事で取り上げようと思います。

動作変更

stale-while-revalidate(RFC5861)に対応しました
graceの初期値に使う感じです。

VCL変更/アクションのリターン値

vcl_hit
return(fetch)はreturn(miss)に変更する必要があります。
ただし4.1.0時点では変更を促すログを出しますがfetchはmissと同じように動作します
恐らく次の大型バージョンアップ時で消されるのではないかと思います。

vcl_backend_error
return(abandon)が追加されました。

vcl_init
return(fail)が追加されました。
文字通りfailを返すとvclの初期化に失敗します。

VCL変更/変数

remote.ip / local.ip (IP READ)
先程も述べたとおりPROXY Protocolに対応したため、それに伴いclient.ip/server.ipにPROXYサーバから渡された値が入ってくるようになりました。
(※ここで言うPROXYサーバはhaxproxyだったりAWSのELB等のPROXY Protocolを喋る上位サーバです。)
そのためclient.ipがVarnishに接続してきたIPアドレスを示すのではなくPROXYサーバがに接続してきたIPアドレスが入ってくる可能性が出てきました。
言葉で説明するよりマトリックスで示したほうが分かりやすいので以下に示します。


Direct
[client(192.168.1.200)]
   ↓
[Varnish(192.168.1.100:6081)]

Proxy Protocol
[client(192.168.1.200)]
   ↓
[HAProxy(192.168.1.10:8080)]
   ↓
[Varnish(192.168.1.100:6086)]

Description Direct PROXY Protcol
remote.ip ローカル(Varnish)に接続してきたIPアドレス 192.168.1.200(Client) 192.168.1.10(HAProxy)
client.ip クライアントのIPアドレス 192.168.1.200(Client) 192.168.1.200(Client)
local.ip ローカル(Varnish)のIPアドレス 192.168.1.100:6081(Varnish) 192.168.1.100:6086(Varnish)
server.ip クライアントのコネクションを受けたIPアドレス 192.168.1.100:6081(Varnish) 192.168.1.10:8080(HAProxy)

req_top.* (READ)
トップリクエストのreq.*を参照します、書き込みは出来ません。
何に使えるかというとESIの時に親(top)のreq.urlを知りたいといった時です。
取得できるのはあくまでもリクエストに関わるものなので例えばreq_top.backend_hintみたいなものはありません。
使用できるのは以下です。
req_top.method
req_top.url
req_top.http.*
req_top.proto

beresp.was_304 (BOOL READ)
文字通りbackendから304(Not Modified)が返却され、既に取得しているオブジェクトのリフレッシュに成功した場合にtrueになります。

beresp.age / obj.age (DURATION READ)
オブジェクトが生成されてからの経過時間(Age)を返却します。

beresp.backend (BACKEND READ)
fetchに利用したバックエンドを返却します。
例えばバックエンド名を取得したい場合はberesp.backend.nameとします。

resp.is_streaming (BOOL READ)
ストリーミングされている場合にtrueになります。

パラメータの変更

変更されたもの
workspace_session
4.0.3: 384byte
4.1.0: 512byte

vsl_mask
4.0.3: -VCL_trace,-WorkThread,-Hash
4.1.0: -VCL_trace,-WorkThread,-Hash,-VfpAcct
※VfpAcctは4.1.0で追加されたログ項目

削除されたもの
group
group_cc
listen_address
user
pool_vbc
timeout_req
timeout_idleと統一

追加されたもの
vcl_cooldown
VCLがcold状態に遷移するまでの時間

vmod_std

real2integerが追加されました。
型変換関数でfallbackがなかったものにfallbackが追加されました(time2integer / time2real / real2time)

varnishstat

-wがなくなりました

varnishhist / log / ncsa / top

-tオプションが追加されました(VSMを開く際のタイムアウト設定)

その他

vmodを追加しました
追加したといってもデフォルトで入っているわけではなく別reposです
libvmod_saintmode
Varnish3.0系であったsaintmodeです

libvmod_xkey
セカンダリハッシュを作成します。主にパージに利用し高速に動作します。
恐らくですがVarnishPlusで提供されていたhash-ninjaと同等のものと考えています。

libvmod-rtstatus
varnishstatと同等のデータを出力します

リンク

https://www.varnish-cache.org/docs/trunk/whats-new/changes.html
https://github.com/varnish/Varnish-Cache/blob/4.1/doc/changes.rst


3月 302015
 

Varnish3.0.7がリリースされました。
Version3.0.xのサポート期限が4月までのため流石にもう追加リリースはないんじゃないかと考えています。

なお今回のリリースはバグフィックスと改善になります。
アップデートを強くおすすめします(特にラインセパレータの扱いに対する修正のため)

Changes
ダウンロード


BugFix

1690 – RHEL7/CentOS7環境においてinitスクリプトの問題でうまく動かない問題を修正しました

単一のCR(\r)をラインセパレータとして扱うのを辞めました
通常LFもしくはCRLFをラインセパレータとして使いますがCRも許容していました。
これによりHTTPを解釈する複数のMWでのヘッダの解釈違いを利用することでキャッシュポイズニング攻撃を行うことが可能です。
といっても割と限られた条件なのでめっちゃ緊急というわけでもないです。

クライアントから複数のContent-Lengthを送られてきた場合に400を返すようにしました

1627 – Content-Lengthがおかしいのを修正しました
HTTP/1.0なクライアントでgzip+streamingの場合にContent-Lengthがおかしくなっていたようです。

ban利用時にメモリリークしていたのを修正しました

改善

Hop-by-hop/Endo-to-endヘッダのチェックを強化しました

親-子プロセスにおいて通信の問題が起きた際のエラー検出・再起動プロセスを改善しました


2月 222015
 

Varnish4.0.3がリリースされました。
今回のリリースは公式で行ってる通り多くのfixがありますが重要なバグフィックスを含みます。
比較的遭遇しやすいRaceコンディションに起因するpanicが修正されています。
アップデート時の注意事項はありますが、基本的にアップデートをおすすめします。

Changes
ダウンロード

変更内容から幾つか抜粋して紹介します。


バグフィックス

1650 – 複数のXFFヘッダがあった場合は折りたたむように修正しました

1620 – スレッドプールに空きが無い場合にclientスレッドを使うのを辞めました
空きが無いためbackgroundスレッドを作成出来ない場合にclientスレッドを使っていたのを辞めました。
また、フェッチするためにスレッドがない場合にカウントアップするカウンタfetch_no_threadが追加されました。
これがもし増えるような状況であればスレッド周りのパラメータ調整を行うと良いでしょう。

1566 – VCL中の「?」をエスケープするようにしました

1660 – SynthではRangeリクエストを無視するようになりました。
Synthでレスポンスを返すパスでrangeリクエストを受け付けた場合にassertしていたためです。

1637 – VEP(ESI処理)のコールバックが失敗した際にpanicしていたのをフェッチ失敗(503)として扱うようにしました

1665 – リクエストのタイムアウト計算をドキュメントに合わせました
リクエストのタイムアウトを計算する際にアイドルタイムを基準に計算していましたが
リクエストの最初のバイトを受け取った時間を基準に変更しました。

1672 – バックエンドが意図しない304をレスポンスしてきた場合にpanicしたのを修正しました
バックエンドに対してINM/IMSリクエストをしていないのに304をレスポンスしてきた場合にpanicしたのを修正しました。

1539 – lookupしようとしたオブジェクトがちょうどオブジェクトを開放していくスレッドに変更された際にpanicしてたのを修正しました
感覚的にですが、割と踏む確率が高かったバグです。

1349 – varnishadm:backend.set_healthyのマッチを修正しました

1623 – varnishhist:-dオプション使用時にsegfaultするのを修正しました

1378 – varnishncsa:印字不可能な文字をエスケープして出力するようにしました

1462 – varnishncsa:URLなどリクエストに関わる値は最初のエントリを利用しステータスコードなどレスポンスに関わるものは最後の値を使うように変更しました
いまいちわかりづらいのでIMSリクエストを送った場合のログで例をあげます
■4.0.2/varnishncsa


192.168.1.31 - - [19/Feb/2015:01:47:49 +0900] "GET http://192.168.1.37:6081/x.html HTTP/1.1" 200 0 "-" "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0"

■4.0.3/varnishncsa


192.168.1.31 - - [19/Feb/2015:01:47:16 +0900] "GET http://192.168.1.37:6083/x.html HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0"

■4.0.2/varnishlog -graw -c


         6 Begin          c sess 0 HTTP/1
         6 SessOpen       c 192.168.1.31 59552 :6081 192.168.1.37 6081 1424279850.057539 15
         6 Link           c req 7 rxreq
         7 Begin          c req 6 rxreq
         7 Timestamp      c Start: 1424279850.057624 0.000000 0.000000
         7 Timestamp      c Req: 1424279850.057624 0.000000 0.000000
         7 ReqStart       c 192.168.1.31 59552
         7 ReqMethod      c GET
         7 ReqURL         c /x.html
         7 ReqProtocol    c HTTP/1.1
         7 ReqHeader      c Host: 192.168.1.37:6081
         7 ReqHeader      c User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0
         7 ReqHeader      c Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
         7 ReqHeader      c Accept-Language: ja,en-us;q=0.7,en;q=0.3
         7 ReqHeader      c Accept-Encoding: gzip, deflate
         7 ReqHeader      c Connection: keep-alive
         7 ReqHeader      c If-Modified-Since: Wed, 18 Feb 2015 16:43:37 GMT
         7 ReqHeader      c If-None-Match: "280ea4-b-50f5f855c1b9e"
         7 ReqHeader      c Cache-Control: max-age=0
         7 ReqHeader      c X-Forwarded-For: 192.168.1.31
         7 VCL_call       c RECV
         7 VCL_return     c hash
         7 ReqUnset       c Accept-Encoding: gzip, deflate
         7 ReqHeader      c Accept-Encoding: gzip
         7 VCL_call       c HASH
         7 VCL_return     c lookup
         7 Debug          c "XXXX MISS"
         7 VCL_call       c MISS
         7 VCL_return     c fetch
         7 Link           c bereq 8 fetch
         7 Timestamp      c Fetch: 1424279850.058979 0.001355 0.001355
         7 RespProtocol   c HTTP/1.1
         7 RespStatus     c 200★←これが使われた
         7 RespReason     c OK
         7 RespHeader     c Date: Wed, 18 Feb 2015 17:17:30 GMT
         7 RespHeader     c Server: Apache/2.2.22 (Ubuntu)
         7 RespHeader     c Last-Modified: Wed, 18 Feb 2015 16:43:37 GMT
         7 RespHeader     c ETag: "280ea4-b-50f5f855c1b9e"
         7 RespHeader     c Vary: Accept-Encoding
         7 RespHeader     c Content-Encoding: gzip
         7 RespHeader     c Content-Type: text/html
         7 RespHeader     c X-Varnish: 7
         7 RespHeader     c Age: 0
         7 RespHeader     c Via: 1.1 varnish-v4
         7 VCL_call       c DELIVER
         7 VCL_return     c deliver
         7 Timestamp      c Process: 1424279850.059049 0.001425 0.000070
         7 RespProtocol   c HTTP/1.1
         7 RespStatus     c 304★←本当はこれを使ってほしい
         7 RespReason     c Not Modified
         7 RespReason     c Not Modified
         7 Debug          c "RES_MODE 0"
         7 RespHeader     c Connection: keep-alive
         7 Timestamp      c Resp: 1424279850.059136 0.001513 0.000087
         7 Debug          c "XXX REF 2"
         7 ReqAcct        c 423 0 423 315 0 315
         7 End            c

■4.0.3/varnishlog -graw -c


     32774 Begin          c sess 0 HTTP/1
     32774 SessOpen       c 192.168.1.31 59586 :6083 192.168.1.37 6083 1424279998.331551 15
     32774 Link           c req 32775 rxreq
     32775 Begin          c req 32774 rxreq
     32775 Timestamp      c Start: 1424279998.331637 0.000000 0.000000
     32775 Timestamp      c Req: 1424279998.331637 0.000000 0.000000
     32775 ReqStart       c 192.168.1.31 59586
     32775 ReqMethod      c GET
     32775 ReqURL         c /x.html
     32775 ReqProtocol    c HTTP/1.1
     32775 ReqHeader      c Host: 192.168.1.37:6083
     32775 ReqHeader      c User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0
     32775 ReqHeader      c Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
     32775 ReqHeader      c Accept-Language: ja,en-us;q=0.7,en;q=0.3
     32775 ReqHeader      c Accept-Encoding: gzip, deflate
     32775 ReqHeader      c Connection: keep-alive
     32775 ReqHeader      c If-Modified-Since: Wed, 18 Feb 2015 16:43:37 GMT
     32775 ReqHeader      c If-None-Match: "280ea4-b-50f5f855c1b9e"
     32775 ReqHeader      c Cache-Control: max-age=0
     32775 ReqHeader      c X-Forwarded-For: 192.168.1.31
     32775 VCL_call       c RECV
     32775 VCL_return     c hash
     32775 ReqUnset       c Accept-Encoding: gzip, deflate
     32775 ReqHeader      c Accept-Encoding: gzip
     32775 VCL_call       c HASH
     32775 VCL_return     c lookup
     32775 Debug          c "XXXX MISS"
     32775 VCL_call       c MISS
     32775 VCL_return     c fetch
     32775 Link           c bereq 32776 fetch
     32775 Timestamp      c Fetch: 1424279998.333049 0.001412 0.001412
     32775 RespProtocol   c HTTP/1.1
     32775 RespStatus     c 200
     32775 RespReason     c OK
     32775 RespHeader     c Date: Wed, 18 Feb 2015 17:19:58 GMT
     32775 RespHeader     c Server: Apache/2.2.22 (Ubuntu)
     32775 RespHeader     c Last-Modified: Wed, 18 Feb 2015 16:43:37 GMT
     32775 RespHeader     c ETag: "280ea4-b-50f5f855c1b9e"
     32775 RespHeader     c Vary: Accept-Encoding
     32775 RespHeader     c Content-Encoding: gzip
     32775 RespHeader     c Content-Length: 31
     32775 RespHeader     c Content-Type: text/html
     32775 RespHeader     c X-Varnish: 32775
     32775 RespHeader     c Age: 0
     32775 RespHeader     c Via: 1.1 varnish-v4
     32775 VCL_call       c DELIVER
     32775 VCL_return     c deliver
     32775 Timestamp      c Process: 1424279998.333124 0.001487 0.000075
     32775 RespProtocol   c HTTP/1.1
     32775 RespStatus     c 304★←これを使った(最後のエントリ)
     32775 RespReason     c Not Modified
     32775 RespReason     c Not Modified
     32775 RespUnset      c Content-Length: 31
     32775 Debug          c "RES_MODE 0"
     32775 RespHeader     c Connection: keep-alive
     32775 Timestamp      c Resp: 1424279998.333257 0.001620 0.000133
     32775 Debug          c "XXX REF 2"
     32775 ReqAcct        c 423 0 423 319 0 319
     32775 End            c


4.0.2の場合はステータス200でLength:0で帰ってきています。
これは最初に現れたRespStatusを利用したためです
期待しているのはdeliverする直前、つまり最後の値なわけで4.0.3では最後のRespStatusの304を出力するようになっています。

機能追加

vmod_std: VOID std.cache_req_body(BYTES)が追加されました
使い方については以前書いたこちらの記事を参照してください(Varnishでテストコードを書こう!~実践編~+Bodyを読もう!

vmod_std: TIME std.time(STRING,TIME)が追加されました


//VCL
std.log(""+now);
std.log(""+std.time("hogehoge",now));//無効な文字列を指定すると第二引数が使われる
std.log(""+std.time("Sun, 22 Feb 2015 00:0:00 GMT",now));

//結果
//-   VCL_Log        Sun, 22 Feb 2015 01:21:31 GMT
//-   VCL_Log        Sun, 22 Feb 2015 01:21:31 GMT
//-   VCL_Log        Sun, 22 Feb 2015 00:00:00 GMT

使用可能なフォーマットは


"Sun, 06 Nov 1994 08:49:37 GMT"
"Sunday, 06-Nov-94 08:49:37 GMT"
"Sun Nov  6 08:49:37 1994"
"1994-11-06T08:49:37"
"784111777.00"
"784111777"

です

VCL変更

bereq.uncacheableがread-onlyになりました

obj.uncacheableがvcl_hitではなくvcl_deliverで使えるようになりました

パラメータ変更

fetch_chunksizeが128KBから16KBに変更されました

アップデート時の注意

fileストレージを利用している場合で空き容量が指定サイズを上回っている場合に
「larger than file system」と出て起動に失敗します。(関係するコミット
Varnishのfileストレージを利用する場合にフラグメントを防ぐためにddで先にファイルを作っておく事を公式でも推奨していますが
既に確保しているサイズを考慮していないため、ddで事前確保しようとするとディスクサイズの50%以下しか使用できません。
一旦消してしまえば問題なく起動しますが注意が必要です。
また、この問題は当然ですが4.0.3にアップデートしたあとにrestartを行っても起きるので注意してください。(reloadは問題なしです)
あとでチケットきろうかなと思います。


1月 252015
 

小ネタです
Varnishを使う上で冗長化をどうしようと悩むことが多々有ります。
単純に横に並べてLBでバランシングしてもキャッシュの同期をどうしようという問題にぶち当たります。
VarnishSoftwareがサブスクリプションで提供しているVarnishPlusでは同一階層のVarnishにおいてキャッシュオブジェクトのレプリケーションを行うVarnish High Availabilityという機能が存在しますがコミュニティ版のVarnishでは存在しません。
(VarnishPlusについてはそのうち記事書こうと思います)
強引にVCLでSquidのsiblingのような動きをするように書くことも出来なくないのですが個人的にはオススメできません。
幾つか理由があるのですが一番大きい理由がRace conditionに陥るからです。
VarnishはこれはThundering Herd問題に対処するために同時に同じリクエストが来た場合でもバックエンド/オリジンに行くリクエストはひとつです。
簡単にいうとロックしています。これは単体サーバではうまく動きますが同一階層においてキャッシュを同期しようとすると問題が起きます。
hib
図のように同時に同じリクエストが来て、どちらもオブジェクトを持っていない場合にRace conditionになります。
これを防ぐためにreq.hash_ignore_busyをtrueにするという手もありますが台数が増えた時にどうするかとか運用が手間ですし、VCLをミスったら破綻するような危険を持つべきではないと考えています。
つまり同一階層においてキャッシュを安全かつ簡単に同期する方法はVarnishPlus以外に存在しません。
また、Varnishはrestartすると基本的にキャッシュがすべて吹き飛びます。
persistentもあるじゃないかという話もありますが非推奨になったうえ運用上ケアすべき問題が多く癖が強過ぎて普通の人には使いづらいです。

これらも含めて様々な問題に対処するために多段構成を組むことがあります。

  • 全体でのキャッシュ同期
  • 重複リクエストによるオリジン負荷
  • サーバダウン時のオリジン負荷
  • 効率的なキャッシュの保持

上記は問題の一部ですが1つずつ解説します。

全体でのキャッシュ同期
全体でキャッシュが同期されているということはどういうことでしょうか?
あくまで個人的な考えですが、全体でTTLの整合性がとれていることだと考えています。
例えばTTLが60秒のオブジェクトとVarnish2台で考えてみましょう

  1. Varnish-1にアクセスしてキャッシュされる(TTL=60s)
  2. 30秒まつ
  3. Varnish-2にアクセスしてキャッシュされる(TTL=30s)
  4. さらに30秒後
  5. Varnish-1/2両方でキャッシュがexpireする

こんなかんじです。
つまり最初にアクセスされた時間を全体で把握していて、TTL内であればキャッシュを保持していないインスタンスでも経過時間を減算しておくということです。
これは静的コンテンツで上書きをしないのであればさほど考える必要はありませんが(消されたらbanすればOK)動的コンテンツの場合は注意を払う必要があります。
こんなケースを考えてみましょう

  • 10分毎に更新されるランキングページがある
  • しかし実際はアクセスされる度に集計されてVarnish側でTTLを10分としている

この場合で同期がとれていない場合アクセスの度に最新のランキングだったり、少し前のランキングだったりと目まぐるしく変わる可能性が高いです。
そこで多段構成です。
tiv
Varnishの標準で用意されているdirectorはランダムやハッシュ等で振り分けが可能です。
Varnishがキャッシュオブジェクトを特定するのはHostとURLを使っています。(server.ipも使っては居ますがここでは一旦置いておきます)
そこで同じキーを使ってハッシュ振り分けを行うことで同じHostとURLを保つ場合は常に同じ2段目のVarnishにアクセスします。


vcl 4.0;
import directors;
probe healthcheck {
    .request =
            "GET /healthcheck/check.html HTTP/1.1"
            "Host: xxxx.xxxx"
            "Connection: close";
    .timeout           = 2s;
    .window            = 5;
    .threshold         = 3;
    .interval          = 1s;
}

backend ws01 {.probe=healthcheck;.host = "192.168.1.1";.port = "80";}
backend ws02 {.probe=healthcheck;.host = "192.168.1.2";.port = "80";}

sub vcl_init{
  new ws_hash = directors.hash();
  ws_hash.add_backend(ws01, 1.0);
  ws_hash.add_backend(ws02, 1.0);
}
sub vcl_recv{
  set req.backend_hint = ws_hash.backend(req.url + ":" + req.http.host);
}

Varnishはキャッシュしてからの経過時間であるAgeヘッダをレスポンスし、またこれを解釈してTTLから減算します。
つまり図のような構成でHash振り分けを行った場合(TTLは60秒とします)

  1. (黒線)/hogeにアクセスする。
  2. 1Aにキャッシュがないので2Aにリクエスト
    1. 2Aにキャッシュがないのでオリジンにリクエスト
    2. 2Aでキャッシュする(TTL=60s/Age=0s)
    3. 1Aでキャッシュする(TTL=60s/Age=0s)
  3. 10秒待つ
  4. (赤線)/hogeにアクセスする。
  5. 1Bにキャッシュがないので2Aにリクエスト
    1. 2Aがレスポンス(Age=10s)
    2. 1Bでキャッシュする(TTL=60s/Age=10s)
  6. 10秒待つ
  7. (青線)/hogeにアクセスする。
  8. 1Cにキャッシュがないので2Aにリクエスト
    1. 2Aがレスポンス(Age=20s)
    2. 1cでキャッシュする(TTL=60s/Age=20s)
  9. 40秒後
  10. 1A/1B/1C/2AにおいてTTL=AgeとなりキャッシュがExpireする

(Expire周辺の計算は変数が多くわりかし複雑なんですがここでは単純化しています)
このようにすべてのオブジェクトが同時に消えることがある程度期待できます。
ここである程度としているのはexpire前にnukeしてしまったり、1段目に行き渡ってない状態で2段目が死んだりした時のことは考えていないからです。
これも考慮に入れる必要がある場合は動的コンテンツ側で適切なヘッダをつける必要があるでしょう。

重複リクエストによるオリジン負荷
Varnishを複数並べる理由はいくつかあります。冗長構成を取るためにだったり、トラフィックが増えてきたのでそれを捌くための増設だったりです。
dup
単純に横に並べてしまうと最悪、同じリクエストで最大並べた台数分のリクエストが来る可能性があります。
これも多段構成にすることで解決できます。
tiv
同一リクエストは2段目で必ず同一サーバを経由するために1段目がいくら増えようともオリジンに行くリクエストは1つです。

サーバダウン時のオリジン負荷
キャッシュサーバが落ちれば当然ですがキャッシュが無くなるので再度オリジンに取得しに行きます。
fail
キャッシュに依存しているシステムほどキャッシュが吹き飛んだ時にオリジンの負荷が一気に上がり負荷が増え、最悪の場合連鎖障害になることが有ります。
しかし多段構成を組んでいる場合は余り影響を受けない、もしくは影響を小さくすることが出来ます。
1段目が死亡しても2段目がキャッシュを保持しているのでオリジンの負荷はそこまで増えません。
同じように2段目が死亡しても1段目がキャッシュを保持しているのでオリジンの負荷の上がり方はある程度抑えられます。

効率的なキャッシュの保持
1段目はクライアントからの激しいリクエストを受けるため、高速なstorage(mallocやSSD/PCIeSSDなどのfile)が必要です。
ここはいくらサーバを増やしてもキャッシュの保持容量は単一サーバでのstorageサイズとなります。ランダムにそれぞれのサーバにリクエストされるためです。
もちろん現金で殴るという手段も取れなくはないのですが(僕を現金で殴ってくれる人募集しています)、1台落ちるとわりかし被害が大きくなりやすいのでそこはバランスをみてやるべきでしょう。
多段構成の場合で2段目は多少遅いstorageでも問題がありません。(とはいってもSSDはほしいです)
理由は既に1段目である程度のリクエストをシェーブしているのと、よくアクセスされるオブジェクト(=ホットデータ)はほぼほぼ1段目に集中することが期待できるため全体で高速にレスポンスすることが可能です。
また、ハッシュ振り分けを行う場合は当然ですが2段目で重複オブジェクトを持ちません。
そのためキャッシュの保持容量は単純に足したサイズとなり、よりオリジンの負荷軽減に役に立ちます。

ここまで多段構成イイヨーイイヨーという話をしましたが多段構成でも注意すべきところがあります。

  • AppとVarnishが同居している多段構成においての振り分けについて

AppとVarnishが同居している多段構成においての振り分けについて
sep
例えば上図のようにOrigin(App)が分離しているケースは問題ありませんが
nsep
このように2段目のVarnishがAppと同居しているケースを考えてみましょう。(2段目のVarnishは必ずlocalのappにリクエストを行う)
当然ながら1段目はハッシュで振り分けを行っています。
そして動的コンテンツの場合はすべてのリクエストをキャッシュ出来ないことが多いです。
むしろキャッシュ出来ないものが多いケースのほうが多いと思います。
当然ながらキャッシュ出来ないリクエストはオリジンに直撃します。
そしてたいていの場合キャッシュ出来ないリクエストは

  • 会員情報を扱っていてユーザ毎に内容が異なる
  • POSTやPUTなどそもそもオリジンに確実にリクエストを通さないと行けない
  • などなど

だったりでだいたい同じURLだったりします。
URLが同じということはハッシュが同一ということなので負荷が寄ります。
せっかく負荷を減らすために多段にしたのに本末転倒といえるでしょう。
じゃぁどうするかというとキャッシュするリクエストとキャッシュしないリクエストで振り分けを変えることです。


vcl 4.0;
import directors;
probe healthcheck {
    .request =
            "GET /healthcheck/check.html HTTP/1.1"
            "Host: xxxx.xxxx"
            "Connection: close";
    .timeout           = 2s;
    .window            = 5;
    .threshold         = 3;
    .interval          = 1s;
}

backend ws01 {.probe=healthcheck;.host = "192.168.1.1";.port = "80";}
backend ws02 {.probe=healthcheck;.host = "192.168.1.2";.port = "80";}
backend ws03 {.probe=healthcheck;.host = "192.168.1.3";.port = "80";}
backend ws04 {.probe=healthcheck;.host = "192.168.1.4";.port = "80";}

sub vcl_init{
  new ws_hash = directors.hash();
  ws_hash.add_backend(ws01, 1.0);
  ws_hash.add_backend(ws02, 1.0);
  ws_hash.add_backend(ws03, 1.0);
  ws_hash.add_backend(ws04, 1.0);

  new ws_rand  = directors.random();
  ws_rand.add_backend(ws01, 1.0);
  ws_rand.add_backend(ws02, 1.0);
  ws_rand.add_backend(ws03, 1.0);
  ws_rand.add_backend(ws04, 1.0);
}
sub vcl_recv{
  ...
  if(キャッシュするリクエストの場合){
    //キャッシュする
    set req.backend_hint = ws_hash.backend(req.url + ":" + req.http.host);
    return(hash);
  }else{
    //キャッシュしない
    set req.backend_hint = ws_rand.backend();
    return(pass);
  }
}

hr
こうすることでキャッシュも効率的に行なえますし、負荷も適切に割り振り出来ます。

まとめ
多段構成を行うことで幸せになれるポイントを示せたんじゃないかなと思います。
もちろんデメリットがゼロかというとそうではなく、多段にすることで経由するサーバが増えるためその分latencyは悪化しますが、大抵の場合はそれを補う効果が得られます。
これらのメリットや考えるポイントはごく一部で他にも地域を考慮したりとか3段目つくったりとか、ホットデータ専用の隔離を作ったりとかいろいろ行うことによっていろいろ違います。
これらの階層構造は何も自社環境だけで留まるわけではなく、各CDNもまたひとつの層と考えて最適な構造を考えるのも面白いと思います。

※注意事項
現在(4.0.2)のVarnishですがヘルスチェックで引っかかって振り分け落とされた負荷がそのまま特定のバックエンドに寄るというバグが有ります。
masterでは修正されていますが4.0.3に適用されるか微妙なので注意が必要です。
また修正後についてもハッシュ振り分けについてはfail時の振り分けで全体で再計算が走るので同様に注意が必要です。
本家ではデフォルトで提供するのはシンプルにしたいということでいわゆるconsistent hashingはサポートしないと言っているので
そのような機能があるvmod(vslp)を使うと良いと思います


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


    10月 182014
     

    Varnish4.0.2がリリースされました。
    多くのバグフィックスと機能追加・改善・修正、ドキュメント改善などを含むためバージョンアップを強く薦めます。
    ちなみにvarnishstatのヒットレート表示も復活しています。

    Changes
    ダウンロード

    変更内容から幾つか抜粋して紹介します。


    バグフィックス

    ESIのメモリリークを修正

    deliver時に競合状態陥る可能性があるバグを修正
    チケット報告はされていないのできわめて稀なケースだとは思います。

    再利用を行う変数の初期化不足で落ちるケースがあるのを修正しました(#1553)

    purge時にworkspaceを使いきって落ちるのを修正しました(#1551)

    varnishtopで正しくグループ化されないのを修正しました(#1591)

    VMOD_ABIのバージョン要件が4.0.1で緩和されたはずなのにされていなかったのを修正しました(#1538)

    varnishncsaでHTTPでない壊れたリクエストが来た場合に出力しないようにしました(#1584)

    max-ageとageが存在する場合でTTL計算時にageが二重で効いていたのを修正しました(#1578)
    例えばmax-ageが10でageが2だった場合、期待されるttlは8ですがageが二重で効いて6になってました。

    director.hashでバックエンドを取得する際に存在しない変数を指定すると落ちるのを修正しました(#1568)
    hashディレクターで取得する際にreq.http.cookieのようにリクエストによっては存在しないものを指定すると落ちましたがそれを修正。

    vcl_backend_responseからretryするとbereqの変更内容が消えるのを修正しました(#1512)
    この修正と一緒にbackendスレッドでロールバックを行うと落ちる問題も修正されました。

    リクエストのbodyを読みきれなかった場合で落ちるケースが有るのを修正しました(#1562)

    shm_reclenを増やすと落ちるケースが有るのを修正しました(#1547)

    random/hashディレクターでsickなbackendにリクエストを投げるケースがあるのを修正しました(#1575)

    varnishtest実行時にバッファ不足でassertが出るのを改善しました
    varnishtestのログのバッファサイズは256KB持っているのですが
    出力されるログが多すぎると以下の様なエラーがでるので512KBまで拡張しました。


    パラメータ追加・変更

    追加:group_cc
    vclをコンパイルする際に利用するcc_commandを実行するグループを指定できます。

    名前変更:vsl_reclen(旧名shm_reclen)
    まだshm_reclenは残っていますがそのうち消えると思うので使っている場合は変えましょう。

    値変更:workspace_client
    最小値が3KBから9KBに増えました。


    機能追加・改善・修正

    vmod_std:querysortでのキー数制限(32)がなくなりました
    workspaceを使うようになっていますのですごく大きなキーを変更する可能性がある場合は大きめにすると良いです。

    vclにHTTP型が追加されました
    reqやbereqなどをまるごとvmodに渡すようなことが出来るようになりました。

    vmodにBYTES型が追加されました
    もともとvclではあったBYTESですが、型変換無しでvmodに渡せるようになりました。

    rollbackがstd.rollbackに移動しました
    rollbackは非推奨になります。

    vcl_deliverでsynthが使えるようになりました

    varnishstatのhitrate表示が復活しました
    よかった・・・

    組み込みのエラーページがvalid HTML5になりました

    server.(hostname|identity)がすべてのファンクションで使えるようになりました
    4.0.1まではclientスレッドのファンクションでしか使えませんでした。

    vmod_std:文字列検索をするstrstrが追加
    使い方は通常のstrstrと同じです。
    大文字小文字は区別されますので必要に応じてstd.tolowerを使うなどで揃えると良いです。

    
    //STRING strstr([検索対象文字列], [検索文字列])
    
    //req.url中に/admin/が含まれているかをチェック
    if(std.strstr(req.url, "/admin/")){
      //found
      ...
    }else{
      //notfound
      ...
    }
    
    

    varnishlog:-kオプションが復活
    指定個数のトランザクションを表示したらexitするオプションです

    varnishadm:vcl.showでincludeされているすべてが表示できる-vオプションが追加
    varnishadmで現在loadされているvclのリストを出力するvcl.showというコマンドがあるのですが
    一つ困ったところにinclude先が表示されないという問題がありました。
    しかし今回サポートされた-vオプションでその名前でloadされているvclの全情報(builtin.vcl含む)が表示されるようになりました。

    
    [root@cache01 ~]# varnishadm vcl.show -v boot
    
    // VCL.SHOW 0 110 input
    vcl 4.0;
    import std;
    import directors;
    include "/etc/varnish/backend.vcl";
    include "/etc/varnish/main.vcl";
    
    // VCL.SHOW 1 5479 Builtin
    ...中略...
    
    // VCL.SHOW 2 1180 /etc/varnish/backend.vcl
    probe healthcheck {
    ...中略...
    
    // VCL.SHOW 3 3179 /etc/varnish/main.vcl
    sub vcl_synth{
    ...中略...
    
    

    といった感じです。
    コメントのVCL.SHOWは以下の情報を示します
    // VCL.SHOW [srcbodyのインデックス番号] [文字列長] [srcname]
    まずVCLはDSLで実行前にCのコードに変換されるのですが、その際にVCL_confという構造体に各種の情報が突っ込まれていてインデックス番号とsrcnameはそこの情報になります。

    
    varnishd -d -f /etc/varnish/default.vcl -C
    の出力から抜粋
    const char *srcname[4] = {
            "input",
            "Builtin",
            "/etc/varnish/backend.vcl",
            "/etc/varnish/main.vcl",
    };
    const char *srcbody[4] = {
        /* "input"*/
            "vcl 4.0;\n"
            "import std;\n"
    ...
            "",
        /* "Builtin"*/
            "/*-\n"
            " * Copyright (c) 2006 Verdens Gang AS\n"
    ...
            "}\n"
            "",
        /* "/etc/varnish/backend.vcl"*/
            "probe healthcheck {\n"
    ...
            "",
        /* "/etc/varnish/main.vcl"*/
            "sub vcl_synth{\n"
    ...
            "",
    };
    ...
    const struct VCL_conf VCL_conf = {
            .magic = VCL_CONF_MAGIC,
            .init_vcl = VGC_Init,
            .fini_vcl = VGC_Fini,
            .ndirector = 9,
            .director = directors,
            .ref = VGC_ref,
            .nref = VGC_NREFS,
            .nsrc = 4,★VCLの個数
            .srcname = srcname,★名前(inputはルートのVCL、builtinはVarnishのデフォルトの動作を定義したもの、ファイルパスのものはincludeしたもの)
            .srcbody = srcbody,★VCLそのもの
            .recv_func = VGC_function_vcl_recv,
            .pipe_func = VGC_function_vcl_pipe,
            .pass_func = VGC_function_vcl_pass,
            .hash_func = VGC_function_vcl_hash,
            .purge_func = VGC_function_vcl_purge,
            .miss_func = VGC_function_vcl_miss,
            .hit_func = VGC_function_vcl_hit,
            .deliver_func = VGC_function_vcl_deliver,
            .synth_func = VGC_function_vcl_synth,
            .backend_fetch_func = VGC_function_vcl_backend_fetch,
            .backend_response_func = VGC_function_vcl_backend_response,
            .backend_error_func = VGC_function_vcl_backend_error,
            .init_func = VGC_function_vcl_init,
            .fini_func = VGC_function_vcl_fini,
    };
    
    

    直近のVDDによると次は4.1でが予定されていて速くて年内に出る可能性があります。