Varnishでテストコードを書こう!~実践編~+Bodyを読もう!
こんにちは、Service Reliabilityチームのいわなちゃんさん(@xcir)です。
先日、社内で行われたチューニングハッカソンではチームマイナス5500万としてVarnishをいれる簡単なお仕事をしてきました。
このエントリは GREE Advent Calendar 2014 17日目の記事です。
以前とある勉強会で
「varnishtestの使い方がよくわからない」
「別のツールを使ってテストをやっている」
という話を聞きました。
varnishはvarnishtestというテストツールがありますが、あまり利用されていないようです。
原因はいくつかあると思いますが、まずは実際のテストコード(VTC)を見てみましょう。
以下は公式のrollback機能のVTCです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
varnishtest "Test Rollback" server s1 { rxreq expect req.url == "/foo" expect req.http.foobar == "harck-coff" txresp -status 400 accept rxreq expect req.url == "/bar" expect req.http.foobar == "snark" txresp -bodylen 5 } -start varnish v1 -vcl+backend { sub vcl_recv { if (req.url == "/foo") { set req.http.foobar = "harck-coff"; } } sub vcl_fetch { if (beresp.status == 400) { rollback; set req.url = "/bar"; return (restart); } } } -start client c1 { txreq -url "/foo" -hdr "foobar: snark" rxresp } -run |
https://github.com/varnish/Varnish-Cache/blob/varnish-3.0.6/bin/varnishtest/tests/c00032.vtc
VTC中にVCLが含まれているのがわかります。
varnishd本体のチェックをするのであれば特に問題はありませんが、
VCLのテストを行いたい場合にVTC中にVCLを記載するのは不都合があります。
ではどのように書けば良いのかというのを弊社でのVCLの構造を紹介しつつ一例として紹介します。
なおvarnishtest/VTC自体については以前の記事である「Varnishでテストコードを書こう!」を参照してください。
VCL構造
弊社で運用しているVarnishは100超のドメインを配信しています。
そのため、ドメイン毎にvcl_recvをやらvcl_fetchなどを書いているとキリがありません。
そこでvclの各アクションを複数定義した際に読み込み順に実行される機能(参照)
とincludeを利用し、できるだけ共通化しています。
大まかに以下のように分けています。
- 共通設定
- Probe(ヘルスチェック)やinline-C向けのincludeなどの共通定義
- 正規化やPurgeなど
- ロール別設定
- ACLなどが同じドメインをまとめたタイプ別設定
- 特殊な振り分けルールを持つドメイン別設定
- どのドメインをドメイン・タイプ別設定にルーティングするかの設定
このような構造を取ることで基点となるdefault.vclと各ドメイン毎の設定量は非常に少なくなっています。
弊社で実際に稼働しているvarnishのdefault.vclは以下のような記述になっています。
(※複数のロールがあるのですべてがこの設定ではないです)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
//Load common define include "/etc/varnish/common/common_define.vcl"; //Load common prefilter include "/etc/varnish/common/common_pre.vcl"; //load domain subroutine include "/etc/varnish/ext.vcl"; //load RUM subroutine include "/etc/varnish/rum.vcl"; include "/etc/varnish/is.storage.vcl"; include "/etc/varnish/if.storage.vcl"; include "/etc/varnish/st.storage.vcl"; //load main vcl include "/etc/varnish/main.vcl"; //Load common postfilter include "/etc/varnish/common/common_post.vcl"; |
そしてドメイン・タイプ別設定は
1 2 3 4 5 6 7 8 9 |
sub local_type1_recv { //set default backend set req.backend = stor_image; //アクセス禁止ディレクトリの指定 if(req.url !~ "^/(css|swf|js|img)/") {error 403;} } |
と比較的シンプルです。
ほとんどの基本的な設定はcommon_(pre|post).vclで行っているためです。
このように共通化していると重要なのがテストになります。
特定のドメインへのACLの追加などは影響範囲はそのドメインだけとなりますが、
共通部分に機能を増やそうとすると広範囲に影響を及ぼすためテストが無いと確認は非常に厳しいです。
次に弊社でのVTCの構造と書き方を紹介します。
VTC構造
VTCも共通設定とドメイン・タイプ別設定ごとに分けてテストを行っています。
VTC中にテストを行いたいVCLを書かないようにするためにはincludeを利用しています。
実際に紹介します。
共通設定
vtcサンプル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
server s1 { rxreq txresp -hdr "Set-Cookie: num=123456; expires=Sun, 10-Jun-2001 12:00:00 GMT; path=/HTTP/" -body "aaaaaa" expect req.http.cookie == <undef>; } -start varnish v1 \ -arg "-pvcc_err_unref=off" \ -vcl+backend { include "${gree_confpath}/[共通設定].vcl"; sub vcl_recv{return(pass);} } -start client c1 { txreq -url "/" -hdr "Cookie: Customer=\"Tarou_YAMADA\";" rxresp expect resp.status == 200 expect resp.http.set-cookie == <undef>; } -run |
テスト実行コマンド
1 2 3 |
sudo varnishtest -Dgree_confpath=/etc/varnish *.vtc |
このVTCでは以下のチェックを行っています。
- clientからcookieを送った場合に消去される
- serverからset_cookieを送った場合に消去される
ポイントはreturn(pass)を指定していることと起動パラメータでvcc_err_unref=offを指定していることです。
varnishtestは実際にvarnishを起動して行うので、当然ながらキャッシュされることが有ります。
キャッシュが前提のテストの場合であれば意識してそのように書きますが、
そうでない場合はpassを指定したほうが便利です。
vcc_err_unrefは定義されているのに使われていないbackend設定等がある場合でも警告だけで起動できるので便利です。
また、マクロ(-D)を使っているのは別パスにテストしたいVCLを用意した時のためです。
マクロはVTC内のみで利用できるため、includeした先のVCLでマクロ展開をしようとしても出来ないので注意が必要です。
ドメイン・タイプ設定
vtcサンプル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
varnish v1 -arg "-pvcc_err_unref=off" -vcl { include "${test_conf}"; } -start client c_img { txreq -req GET -url "/img/hoge.gif" -hdr "Host: hogehoge.i.gree.jp" rxresp expect resp.status == "200" expect resp.http.expires != "" } -run client c_e403_1 { txreq -req GET -url "/hogehoge/" -hdr "Host: hogehoge.i.gree.jp" rxresp expect resp.status == "403" } -run |
テスト実行コマンド
1 2 3 |
sudo varnishtest -Dtest_conf=/etc/varnish/default.vcl *.vtc |
このVTCでは以下のチェックを行っています。
- /img/hoge.gifにリクエストしてステータスが200
- /hogehoge/にリクエストしてステータスが403
ここではpassのように無駄なVCLは記述しません。
単純にincludeするだけです。
まとめ
includeをうまく使うことでvarnishtestを使うことが出来ます。
純正のツールなのでテストがFailした時に何処が引っかかったのかわかるので便利だと思います。
なお、今回はvarnish3向けのvtcを書きましたがvarnish4でもほぼほぼ同じです。
varnishtestのおまけ
以前、どこかで与太話として話したのですが
varnishtestはvarnishのテスト目的じゃなくても使えます。
例えばテストでApacheを起動してlocalhost:80に接続してテストを行う場合は以下のように記述できます。
1 2 3 4 5 6 7 8 9 |
shell "/etc/init.d/apache2 start" client c_test -connect "localhost:80"{ txreq -req GET -url "/hogehoge.gif" -hdr "Host: hoge.i.gree.jp" rxresp expect resp.status == "404" expect resp.http.expires != "" } -run |
実行ログ
1 2 3 4 5 6 7 8 9 10 11 |
######## sample.vtc ######## * Starting web server apache2 ######## sample.vtc ######## apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1 for ServerName ######## sample.vtc ######## httpd (pid 17466) already running ######## sample.vtc ######## ...done. # top TEST sample.vtc passed (0.083) |
このような使い方も可能です。
わざわざ他のミドルウェアのテストでvarnishtestを使うこともないと思いますが、
ちょっとしたテストを行う際には便利だと思うので紹介しました。
なお、途中で引っかかった場合は以降のステップは実行されないので、
初期化・終了処理がある場合は何かしらのshellスクリプトから実行するのをおすすめします。
Bodyを読もう!
Varnishを使ってRequest/Response headerを変更することはよくあると思います。
例えば以下の様にです。
1 2 3 4 5 6 |
sub vcl_recv { //静的ファイルへのアクセスの場合はリクエストのクッキーを削除する if(req.url ~ "\.(css|js|png|jpg|gif|ico|json)(\?.*)?$"){ unset req.http.cookie; } } |
同じようにBodyにアクセスできたら非常に便利だと想像できます。
以下はあくまでイメージで実際に動作しないものですがこのような形でできると嬉しかったりします。
1 2 3 4 5 6 7 8 |
sub vcl_recv { //POSTリクエスト中にeval(で始まる文字があったら400で返す if(req.method ~ "POST"){ if(req.body ~ "eval\("){ return(synth(400)); } } } |
このような事をやっているプロダクトの代表例としてはVarnish Security Firewall(VSF)があります。
VSFではvmod-parsereqをRequest bodyにアクセスするために使用していましたがお世辞にも行儀が良いとは言えないもので、どちらかと言うと黒魔術に近いものでした。(参考)
このようにRequest bodyに対して操作を行うのは非常に困難でした。
また同様にResponse bodyについても非常に困難で、本体にパッチを適用しないとアクセス出来ませんでした。
しかしVMOD作者からの要望が届いたのか4.0.xでRequest bodyに対して手軽にアクセス出来る方法が提供されました。
そして次期バージョンの4.1.x(現在trunk)ではResponse bodyに対してもアクセス出来るようになりました。
そこで今回は4.1.xベースで各bodyにアクセスする方法について紹介します。
当然ながら
- ドキュメントに記載されていない
- 4.1は絶賛開発中
- 公式に明確に言及されていない(フックできるポイントを増やすとは言っている程度)
- コアに近い機能
のため突然変更される可能性はありますが、恐らく4.2.xまでは変わっても小規模だろうと踏んでいます。
今回利用したのはRev.7f48458です
vmod自体の作り方についてはlibvmod-example(4.0)を参考にしてください。
Request bodyにアクセスする(VRB)
まずRequest bodyにアクセス出来ると何が出来るかというと
GETだけでなくPOSTリクエスト中にセキュリティ的に問題がないかのチェックが可能になったり
パースすることでPOSTのキーでキャッシュをすることが出来たりします。(あまりおすすめしませんが)
他にもサーバーが503エラーを返した時のリクエストを保存するようなモジュールを書くことが出来ます。
アクセスするのは非常に簡単で、図のようにVRB_CacheでRequest bodyをTransient storageに保存してそれを読み込む形となります。
VRB_Cacheは便利にラップされたstd.cache_req_bodyがあるので今回はそれを使っています。
今回は読み込んだResponse bodyをsyslogに飛ばすモジュールを作ってみましょう。
vmod_test.vcc
1 2 3 |
$Module test 3 test $Function VOID printRequestBody() |
vmod_test.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <stdio.h> #include <stdlib.h> #include <syslog.h> #include "vrt.h" #include "cache/cache.h" #include "cache/cache_filter.h" #include "cache/cache_director.h" #include "vcc_if.h" static int __match_proto__(req_body_iter_f) vbf_printRequestBody(struct req *req, void *priv, void *ptr, size_t l) { CHECK_OBJ_NOTNULL(req, REQ_MAGIC); if (l > 0) { syslog(6,"dump:%d %s",(int)l,(char*)ptr); } return (0); } VCL_VOID vmod_printRequestBody(const struct vrt_ctx *ctx){ VRB_Iterate(ctx->req,vbf_printRequestBody,NULL); } |
default.vcl
1 2 3 4 5 6 7 8 9 10 11 12 |
vcl 4.0; import std; import test; //backendの設定を書く sub vcl_recv{ std.cache_req_body(1MB); } sub vcl_deliver{ test.printRequestBody(); } |
request
1 2 3 4 5 |
POST / HTTP/1.0 Content-Length: 26 Content-Type: text/plain hogehogehoge.magemagemage. |
syslog
1 |
Dec 9 18:19:56 varnish4 varnishd[9504]: dump:26 hogehogehoge.magemagemage. |
無事とれていることが確認できました。
vcl_recvでVRB_Cacheを呼び出せば(今回はstd.cache_req_bodyを利用)
vcl_deliverでもRequest bodyを取得することはできるので、先ほどの使用例にあげた503エラー時に保存することもそこまで難しくありません。
但しファイルに書き込もうとするのはパフォーマンスの劣化に繋がるので避けるのが無難です。
私であればVSLに出力して別のツールで取得して保存を行います。
VSLについてはV3向けですがこちらの記事を参照ください
Response bodyにアクセスする
Response bodyにアクセスできるポイントは2つあります。
1つはfetch時(VFP)
もう1つはクライアントにレスポンスする直前です(VDP)
これらはVarnishの機能にあるESIやgzip/gunzipでも使われています。
ESI/gzipで使われてることからわかるように、どちらも読み書きが可能です。
ではどちらを使えばよいでしょうか?それぞれには次の特徴があります。
VFPの場合はFetch時のStorageに対して読み書きするため1回のみ呼び出されます。
そのためCPUコストが高い処理はこちらでやるとよいでしょう。
VDPの場合はClientにレスポンスするたびに呼び出されます。
しかし実際にレスポンスする内容を見ることが可能です。
例えば、ESIを使っている場合はVFPでは全体像はわかりませんがVDPだと可能です。
またどちらも注意する点が2点有ります。
- Bodyの内容によってVCL内で動作を変えることは出来ない
- オブジェクトはgzipで圧縮されている可能性がある
VFP/VDPはどちらもフィルタを事前に登録しておき、VCLのFunctionを抜けた後に呼び出されるため、Bodyを読み込んだ結果での判定は出来ません。純粋にフィルターとして考えてください。
もちろん登録時にパラメータを渡すことは可能です。
またgzipについては今回はESIのVFP/VDPのフィルタのコードを読むと参考になると思います。
上図はVFP/VDPがどのタイミングでStorageに読み書きをしたり、ClientにResponseするかを示したものです。
着目するべきなのはVFPの一番下層のフィルタ(v1f_(eof|straight|chunked))でBackendからBodyを読み込んでStorageに格納していることです。
v1f_(eof|straight|chunked)は次のフィルタが無いためreturnしていくため、vfp_suckを呼び出した後にbodyにアクセスできます。
逆にVDPの場合は最下層のv1d_bytesでResponseしているため、VDP_Bytesを呼び出す前にbodyにアクセスする必要があります。
それでは実際にVFP/VDPを使ってResponse bodyにアクセスしてみましょう。
Response bodyにアクセスする(Fetch/VFP)
vmod_test.vcc
1 2 3 |
$Module test 3 test $Function VOID HookFetch() |
vmod_test.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#include <stdio.h> #include <stdlib.h> #include <syslog.h> #include "vrt.h" #include "cache/cache.h" #include "cache/cache_filter.h" #include "cache/cache_director.h" #include "vcc_if.h" static enum vfp_status __match_proto__(vfp_pull_f) vmod_vfp_pull_f(struct vfp_ctx *vc, struct vfp_entry *vfe, void *p, ssize_t *lp){ enum vfp_status vp; vp = VFP_Suck(vc, p, lp); syslog(6,"VFP %s",(const char*)p); return (vp); } static enum vfp_status __match_proto__(vfp_init_f) vfp_pull_init(struct vfp_ctx *vc, struct vfp_entry *vfe) { syslog(6,"init"); return (VFP_OK); } static void __match_proto__(vfp_fini_f) vfp_pull_fini(struct vfp_ctx *vc, struct vfp_entry *vfe) { syslog(6,"fini"); } const struct vfp vfp_PREF = { .name = "TEST", .init = vfp_pull_init, .pull = vmod_vfp_pull_f, .fini = vfp_pull_fini, }; VCL_VOID vmod_HookFetch(const struct vrt_ctx *ctx){ (void)VFP_Push(ctx->bo->vfc,&vfp_PREF,1); } |
default.vcl
1 2 3 4 5 6 7 8 9 |
vcl 4.0; import std; import test; //backendの設定を書く sub vcl_backend_response{ test.HookFetch(); } |
request
POSTである必要はないので普通にリクエストしてみてください。
syslog
1 2 3 |
Dec 10 19:50:28 varnish4 varnishd[9459]: init Dec 10 19:50:28 varnish4 varnishd[9459]: VFP <html><body><h1>It works!</h1>#012This is the default web page for this server.</p>#012<p>The web server software is running but no content has been added, yet.</p>#012</body></html>#012 Dec 10 19:50:28 varnish4 varnishd[9459]: fini |
何度かアクセスしてみてください。
VFPが呼ばれるのは一度だけだということがわかると思います。
何かしらの初期化処理が必要な場合は.initで定義したfunctionに終了処理が必要な場合は.finiを使うと可能です。
先ほども説明したとおり、BodyにアクセスできるのはVFP_Suckを呼び出した後なので注意が必要です。
Response bodyにアクセスする(Deliver/VDP)
vmod_test.vcc
1 2 3 |
$Module test 3 test $Function VOID HookDeliv() |
vmod_test.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <stdio.h> #include <stdlib.h> #include <syslog.h> #include "vrt.h" #include "cache/cache.h" #include "cache/cache_filter.h" #include "cache/cache_director.h" #include "vcc_if.h" int __match_proto__(vdp_bytes) VDP_test(struct req *req, enum vdp_action act, void **priv, const void *ptr, ssize_t len) { if (act == VDP_INIT || act == VDP_FINI) return (0); syslog(6,"resp.body(len=%zd) %s ",len,(char*)ptr); return(VDP_bytes(req, act, ptr, len)); } VCL_VOID vmod_HookDeliv(const struct vrt_ctx *ctx){ VDP_push(ctx->req, VDP_test, NULL, 0); } |
default.vcl
1 2 3 4 5 6 7 8 9 |
vcl 4.0; import std; import test; //backendの設定を書く sub vcl_deliver{ test.HookDeliv(); } |
request
POSTである必要はないので普通にリクエストしてみてください。
syslog
1 |
Dec 10 20:02:29 varnish4 varnishd[10487]: resp.body(len=177) <html><body><h1>It works!</h1>#012This is the default web page for this server.</p>#012<p>The web server software is running but no content has been added, yet.</p>#012</body></html>#012 |
何度かアクセスしてみてください。
VFPと違い毎回呼び出されることがわかります。
何かしらの初期化/終了処理が必要な場合はactがVDP_INIT|VDP_FINIのときに処理できます。
VFPと違いFunctionは分かれていません。個人的にリリースされるまでに変更がある場合はここがVFPのように変更されるのでは?と考えています。
また先ほども説明したとおり、BodyにアクセスできるのはVDP_Bytesの呼出し前なので注意が必要です。
まとめ
Varnish4.1(予定)では比較的簡単にRequest/Response bodyにアクセスできることがわかりました。
これを使えばHTMLヘッダ中にjsを挿入したり、自動的に画像等のURLの難読化やショートタイムトークンの付与などが出来たりします。
またこれは私の妄想ですが、VFPで他のリソースをprefetchしてVDPでHTTP/2のpushできたら面白いよなーとか考えています。
prefetchは今も実装はできるのですがパラレル化が黒魔術になりそうだったので、パラレルESIが来るまでまとうと考えています。
他にも使い方はいろいろあると思うのでぜひチャレンジしてみると面白いと思います。
あと本日12/17は私の誕生日だったりします!プレゼントお待ちしております!
明日は@hosi_mo(誕生日)さんによるネイティブゲームクライアントの設計についての記事です。お楽しみに!