Varnishのmoduleを作ってみよう(入門編)

こんにちは、Service Engineeringチームのいわなちゃんさん(@xcir)です。
このエントリはGREE Advent Calendar 2015とVarnish Cache Advent Calendar 2015の17日目の記事です。

はじめに

ApacheやNginxにmoduleがあるようにVarnishにもModule(Varnish Module / VMOD)という仕組みが存在します。
公式に多くのVMODがリストされており機能は多岐に渡ります。
少し紹介してみましょう。

libvmod-digest
hmac_sha256やhash_sha256などの様々なダイジェストを作成するモジュールです。
応用範囲が広くURLの改竄チェックや一定時間有効なURLなど様々なことが可能です。

libvmod-vslp
Varnishで標準で用意している振り分けアルゴリズム(これもDirectorsというVMODで実現されています)はコンシステントハッシュではありませんが、このVMODではサポートしています。

libvmod-redis
名前の通りVCL中からRedisに対してアクセスができます。

libvmod-awsrest
S3などにアクセスするために必要なシグネチャを作成します。

などなど、様々なVMODが存在します。
また、VMODは例えばlibvmod-digestのようにただダイジェストを作るだけのモジュールだとしてもVCLを組み合わせることでいろいろな機能を簡単に実現出来るのも特徴です。
他にもあまりネット上に記事がないため知られていませんが、コア部分を叩くことが無ければ割と簡単にVMODは作ることが可能です。
そこで今回は、実際にlibvmod-dump(以下vmod-dump)というVMODを作成しつつ解説します。

開発環境については以下の通りです。

      Ubuntu 14.04.3 LTS
      Varnish Cache 4.1.0

今回作るvmod-dumpについて

Varnishでデバッグする際はvarnishlogやvarnishncsaを利用してVSLに出力されたログを追っかけます。
例えばエラーが起きた際に再現させるためにrequestの詳細なデータを取得したいことがあります。
以前書いた記事に貼ってある生ログを参照していただきたいのですが、リクエストヘッダは含まれているけどボディが含まれていないことがわかります。
そこで今回のvmod-dumpではこれをdump出来るようにしようと思います。

イメージ的にはこのような感じで、VCLと組み合わせてステータスコードが500以上の時だけdumpするようにしたいと思います。

最初に環境を準備する

3.0.xのころはVMODの開発環境を作るのは割とめんどくさかったのですが最新の4.1.xでは非常に簡単です。
まずは環境を整えつつビルドして動くところまで確認してみましょう。

パッケージのインストール
VMOD作成で最低限必要なパッケージは以下なのでとりあえず入れましょう。

      varnish
      libvarnishapi1
      libvarnishapi-dev

他にもautotools(automake autoconf libtool)やpython-docutilsなどが必要ですが開発を行うマシンであれば大体入っていると思いますので割愛します。
入ってなくても以降の手順中にモジュールが足りないというエラーが出るのでそのタイミングでインストールすれば良いです。

テンプレートの取得(vmod-example)
Varnish公式でVMODを作成するためのテンプレートを用意しています。
まずはこちらをcloneします。

テンプレートをリネーム(rename-vmod-script)
まだcloneしてきただけで、中身はvmod-exampleなので、リネームします。
リネームするためのrename-vmod-scriptというスクリプトが同梱されているのでそれを利用すると一気に変更できて便利です。

ビルドしてインストールしてみる
とりあえずこの状態のままでビルドしてインストールしてみましょう。

エラーが出る場合はだいたいパッケージ不足だと思いますので、エラーの内容にそってパッケージを入れてください。
よくrst2manがないというエラーがでる場合がありますが、python-docutilsを入れれば解決します。

Varnishから動作の確認をしてみる
今ビルドしたvmod_dump(中身はexampleですが)を実際にVarnishから叩いてみましょう。
まずは、使い方を見てみます。

helloファンクションがあるので使ってみます。

動作の確認ができました。
次は実際にコードを書いていきましょう。

コードを書いていく

ディレクトリ構造を見てみよう
VMODを作る上で最低限変更する必要があるファイルが幾つかあります。
まずはディレクトリ構造を見てみましょう。

この中で最低限変更する必要があるのが

      vmod_dump.vcc
      vmod_dump.c

です。
vmod_dump.cはそのままソースコードです。

vccとcを見てみよう

とりあえずvmod_dump.vccを見てみましょう。

このファイルにはVCLから呼び出すための定義や先ほどmanを作成します。(先述)
Prefixに「$」がついているものが実際にコードに影響するもので、それ以外はコメントやmanで使われる部分になります。
では$だけを抜き出すと

になります。
それぞれ説明します。
$Module [module名 ] [セクション番号 ] [簡単な説明(optional) ]
モジュール名になります。
VCLからimportを行う際はここの名前が使用されます。

$Event [ Eventを処理するFunction ]
例えば読み込まれたとか破棄されたとかVMODのライフサイクルにはいくつかのイベントがあります。

イベント名 説明
VCL_EVENT_LOAD VCLが読込された。何かしらの初期化処理をしたい時などに利用
VCL_EVENT_DISCARD VCLが破棄された。不要なリソースの開放などを行う
VCL_EVENT_WARM VCLがWarm状態になった。なんらかのバックグラウンドの処理を行う場合はVCL_EVENT_COLDと合わせて意識する必要がある。
VCL_EVENT_COLD VCLがCold状態になった。
VCL_EVENT_USE VCLが使用開始された(廃止予定)

ファンクションを呼ばれたタイミングですぐ返せるものであればあまりこの辺を意識する必要がありませんが、
例えばバックグラウンドで定期的に外部から情報を取得してきてそれで動きを変えるVMODの場合であればWARM/COLDを意識する必要があるでしょう。
ちなみにWARM/COLDは4.1から追加された機能です。(参考)

見て分かるように変数eにイベントの種類が入っているのでそれで処理する形になります。
privは後でも出てきますが、他のFunctionでも利用できるグローバル変数(PRIV_VCL)みたいなものです。
ちなみにこの$Event定義自体が省略可能です。

$Function [ 戻り値の型 ] [ Function名 ]([ 引数(optional) ],...)
Functionの定義です。
$Function STRING hello(STRING)
に対応するファンクションは以下になります。

vccで定義したファンクション名のprefixにvmod_が付く形になります。
また型ですが以下の種類があります。

vccでの型 コード中での型 オリジナルの定義 説明
BACKEND VCL_BACKEND const struct director * バックエンド定義
BLOB VCL_BLOB const struct vmod_priv * VMOD間でデータのやり取りをするときに使用
BOOL VCL_BOOL unsigned BOOL
BYTES VCL_BYTES double ファイルサイズを指定、VCL中では単位でB,KB,MB,GB,TBが指定可能
DURATION VCL_DURATION double 時間を指定、VCL中では単位でms,s,m,h,d,w,yが指定可能
ENUM VCL_ENUM const char * 列挙型、定義ではENUM { foo, bar }のように指定する
HEADER VCL_HEADER const struct gethdr_s * ヘッダー、req.http.cookieのように指定する
HTTP VCL_HTTP struct http * HTTPヘッダ全体を表す、reqのように指定
INT VCL_INT long 整数
IP VCL_IP const struct suckaddr * IPアドレス
PRIV_CALL struct vmod_priv * 呼び出し毎に有効なプライベートポインタ、同一Functionでも異なる呼び出しではポインタは共有されません、この型はVCL中から指定できません。
PRIV_VCL struct vmod_priv * VCL中で有効なプライベートポインタ、この型はVCL中から指定できません。
PRIV_TASK struct vmod_priv * タスク毎(client/backend)に有効なプライベートポインタ、この型はVCL中から指定できません。
PRIV_TOP struct vmod_priv * リクエスト(ESIのサブリクエストを含む)全体で有効なプライベートポインタ、このポインタはclient側でのみ有効です、この型はVCL中から指定できません。
REAL VCL_REAL double 浮動小数点
STRING VCL_STRING const char * 文字列型
STRING_LIST const char *, ... 文字列型(リスト)
TIME VCL_TIME double 時刻
PROBE VCL_PROBE const struct vrt_backend_probe * ヘルスチェックの定義(probe)
VOID VCL_VOID void VOID

今回は入門編ということもあるので全ての細かい使い方は説明しませんが
PRIV_は使い方が特殊なのとスコープが割と難しいので図解します。
PRIV_はvccで以下のように書いても

VCLから呼び出す場合は

となり、指定できません。VMODでのみ使用が可能です。

PRIV_VCL
pvcl
VCL毎に保持するプライベートのポインタ(以下priv)です。
同一VMODのFunctionなどから同じポインタが使用できます。
Eventで渡されるprivはこれです。
また、VCL毎なので例えばVCLをreloadするなどした場合は当然ながら別のprivになります。
例えば、そのVMOD全体で使う正規表現のコンパイルした結果のストアなどに使用します。

PRIV_CALL
pcall
呼び出し毎に保持するprivです。
リクエストを重ねても同じ呼び出しでは同じprivが返されます。
ただし、同じファンクションでも呼び出し位置が違う場合は別privになります。
例えば、その呼び出し毎にコストがかかる初期化が必要な場合に使用します。

PRIV_TASK
ptask
vxid毎に保持するprivです。
リクエスト毎に作成され、リクエスト間では共有されません。
例えば、クッキーやPOSTのパース結果などに使用します。

PRIV_TOP
ptop
req_top.*変数と同じスコープで保持するprivです。
親子関係を含むリクエスト毎で作成され、関連しないリクエストでは共有されません。
また、backendthread(vcl_backend_responseなど)では利用できません。
例えば、親子間でデータのやり取りする場合に使用します。

ファンクションの定義はこれらを組み合わせて行います。
また、変わった使い方としてですがデフォルト値を指定可能です。

のような指定をすることでVCLから引数を省略したり一部の指定が可能です。

その他
$Objectと$Methodというものがあります。
これはvmod-directorsのようにオブジェクトを作成して動くものに使用されます。
今回は詳しく解説しません。

vmod-dumpのコードを書く
今回リクエストのボディを取得するには、以前記事で紹介した方法(VRB)を使います。
問題はそれをどうやって出力するかです。
例えば直接ファイルに出力することも可能ですが、パフォーマンスの観点からオススメできません。
VMODはVCLから呼び出されますが、VCLに存在するvcl_recvなどの各アクションの中にはレスポンスが終わった後に実施するものがありません。
つまりそこにファイル操作を行ってしまうとその分パフォーマンスが悪化します。
そこで今回はVSLに出力した上で別のコマンドでデータを取得するようにします。
dump
このようなイメージでつくっていきます。

まず、vccを変更します。
(なお、以下のコードはvmod-dumpの開発途中のコードのためバグがあったりします、最新版はこちらを参照ください。)

そしてコードです。

基本的には以前の記事と同等なのですが、幾つかの要素を加えてますので解説します。

VRT_CTX(ctx)
vrt_ctxにはリクエスト・レスポンスヘッダの内容やVCL、workspaceなどの情報が入っています。
Varnishの各種情報にアクセスしたい場合はここを辿ると基本的にすべてあります。
今回リクエストヘッダを取得する際にctx->req->http0を利用していますが、このhttp0はVCLで変更した内容が含まれないオリジナルのヘッダになります。
また、必ずしも構造体にあるデータが全て有効というわけではありません。
わかり易い例だとbackend-threadではctx->reqにはアクセスできません、実際に叩いてみるのが分かりやすいです。

cache_param->vsl_reclen
VSLのラインあたりに出力可能なサイズは決まっており、それを超過すると切り捨てられるため、最大長(vsl_reclen)をパラメータから取得する必要があります。
cache_paramには起動時に指定する各種パラメータが格納されているのでそこからvsl_reclenを取得します。

WS_Reserve, WS_Release(ワークスペース)
Varnish では、セッション毎にワークスペース(crx->ws)を持っています。ここからメモリを確保すれば、Varnish 側で制御してくれるのでメモリリークの心配がなく便利です。
使い方はそこまで難しくありません。
基本的にはWS_Reserveで一時領域を確保してWS_Releaseで固定化もしくは開放します。
vmod-exampleのコードとwsの定義を見てみましょう。

WS_Reserve/WS_Release(おまけでWS_Allocも)を利用した際にどのようにワークスペースの状態が変わるかと構造体内のs,f,r,eポインタの動きを図解します。
ws
一時領域は、使い終わった時点で必ずその領域を返却するかか固定化する必要があります。一時領域を作れるのは一つまでのため、どちらもせずに再度使用開始しようとするとエラーとなります。
また、WS_Reserveで確保したからといって、確保した領域がzero-fillされてはいません。実際にメモリを確保するのではなくポインタを動かしているだけだからです。
ちなみに、今回利用したのは文字列処理を行うための一時領域としてです。なので固定は行っていません。

VSLbt
VSLにデータを書き込む関数です。
他にもVSLbやVSLb_twなどがあります。
また、VSLに書き込む際に通常のVCL_LogタグではなくDebugタグ(SLT_Debug)を利用しています。
これはリクエストボディにはバイナリが含まれる可能性があるため、テキストのみしか許容しないVCL_Logが使えず、許容しているDebugタグを使用したためです。

動作を確認してみる

ビルドして動作をチェックしてみましょう。

backendのサーバは立ち上げなくて大丈夫です。
上がっていなければ503が出るからです。

無事出力できました。

VSLを読むコードを書く

以前の記事で触れたpython-varnishapiを使って作ります。
VSLに吐き出しているのでそれを取得して加工するだけなので割愛します。
コードはこちらにあります。

公開してみる

ここまででとりあえずVMODはできました。
他にテスト(vtc)を作成したり、ドキュメントなどを整備したりする必要がありますが
それが終わったら公式のVMODディレクトリに登録してみるのも良いでしょう。
ディレクトリのページの一番下にsignupの仕方と登録するためのリンクが有りますので、そこから登録しましょう。
VMODの登録は即時ではなく、簡単なレビューを行われて登録されるので数日まって特に問題なければリストに入ります。

vmod-dump自体について

この記事を書いた後も機能拡張をしてまして、レスポンスについてもdumpできるようになっています。(dump.resp)
また、同梱しているvarnishdump.pyを利用することでdumpファイルを取得することができます。

ファイルの中身はまんまHTTPリクエストなのでもちろんそのままncコマンドを利用することで問題の再現が可能です。

レスポンスのdumpもあるのでdiffもとってみました。

(Content-Lengthに違いが出てるのはXIDの桁数が変わった分です)

他にも一定時間経過したリクエストのみをdump出来るように、経過時間を取得するdump.elapsed()もあります。

さらに4.1.0で新たにサポートされたPROXYプロトコルを利用する(-pオプション)ことでclient.ip/server.ipを当初のリクエストと同じ状態も再現することが可能です。
ACL込みで再現試験ができるので捗るような気がします。
よかったら使ってみてください。

最後に

今回はVMODの作成の仕方を紹介しました。
このように、テンプレートのおかげで結構簡単に作れることができます。
皆さんもチャレンジしてみてはいかがでしょうか?

明日の記事は、@hosi_mo(誕生日)さんです。お楽しみに!
ちなみに僕も今日が誕生日です!

リンク

libvmod-dump(今回作ったVMOD)
python-varnishapi(VSLを呼び出すのに使用したライブラリ)
公式のVMODに関するドキュメント
Varnish-Book