7月 302016
 

キャパシティプランニングをする際に頭がいたいものの一つに通常ではないアクセスがあります。
ぱっと思いつくので

  • 閲覧数がページに表示されているのでF5押しっぱなし
  • スクリプトでスクレイピングしようとしているのか暴走している
  • 足跡をつけるために尋常じゃない速度で訪問しまくる
  • ログイン試行
  • 画像をひたすらダウンロード

などなどいろいろあります。
これらに共通なのが、通常ではないリクエストで大量のリソースを消費することです。(もちろん他の問題(セキュリティ)があるものもあります)
もしキャッシュしていたとしても、アウトバウンド帯域を過剰に利用しますし、キャッシュが出来なければwsやdbなどでの負荷になります。
キャパシティプランニングをする際には様々な条件を考えて構築していきます。
単純にユーザーが増えて負荷が増えていくのは望ましく、喜んでインスタンスを増やしたり負荷対策をしますが
そうでない場合は、通常のユーザが巻き添えを食らわないようにキャップしたいものです。
今回は、Varnishでスロットルを行ってみようという記事です。

Varnishでスロットルをかけるのはすごい簡単で、vmod_vsthrottleというVMODを使用します。(ドキュメント)
これはvarnish-modulesに含まれています。
varnish-modulesはVarnish Softwareが作ったVMODで特に他のライブラリに依存しないものを集めたものです。(なのでdigestが入っていない)
ちなみに以下のVMODが含まれています。

  • cookie
  • header
  • saintmode
  • softpurge
  • tcp
  • var
  • vsthrottle
  • xkey

機会があれば、ほかのもそのうち紹介します。

まずはインストールです。
READMEにも書いてあるとおりにインストールします。


xcir@gw01:~$ sudo apt-get install libvarnishapi-dev
xcir@gw01:~$ git clone https://github.com/varnish/varnish-modules.git
xcir@gw01:~$ cd varnish-modules/
xcir@gw01:~/varnish-modules$ ./bootstrap
xcir@gw01:~/varnish-modules$ ./configure
xcir@gw01:~/varnish-modules$ make
xcir@gw01:~/varnish-modules$ sudo make install

./bootstrapでaclocalとかlibtoolizeで引っかかったらautomakeとかlibtoolを入れましょう
インストールはこれだけです。

次に使い方です。


BOOL is_denied(STRING key, INT limit, DURATION period)

keyはクライアントの識別子(IPなど)をいれます。
limitとperiodは組み合わせてつかいます。
period時間中にlimit回数を超えたらアウトみたいな感じです。
要はトークンバケットアルゴリズムです。
なので時間経過で使用した分は回復していきます。(呼び出し時に経過時間を調べて回復させます)
また、limitとperiodの組み合わせで平均RPSとバースト時のRPSを表現できます。
limitがバースト時RPSです。
limit / periodで平均のRPSになります。
なので例えば
limit=6 period=6sであれば平均は1RPSでバースト時は6RPS
limit=6 period=2sであれば平均は3RPSでバースト時は6RPS
みたいな感じになります。
うまく組み合わせて使いたいですね。

また、keyとありますが実際のkey生成にはkey + limit + periodのsha256をとっていますので
同じkeyを指定しても、limitやperiodが変われば別物として扱われます。

とりあえずテストでは/img/以下の画像(jpg|gif|png)を10秒の間に5回リクエストしたら制限をかけて429を返すようにします。
keyはclient.ipです。
image:というprefixをつけているのは、ルールを複数作ることも考えて意図せず被らないようにするためです。


vcl 4.0;
import vsthrottle;

sub vcl_recv {
  if(req.url ~ "^/img/.*\.(jpg|gif|png)" && vsthrottle.is_denied("image:" + client.ip, 5, 10s)) {
    return (synth(429, "Too Many Requests"));
  }
}

早速リロードしてみましょう


xcir@gw01:~/varnish-modules$ sudo varnishncsa -q "requrl ~ '/img/'"
***** - - [30/Jul/2016:01:40:14 +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 200 10355 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
***** - - [30/Jul/2016:01:40:14 +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 200 10355 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
***** - - [30/Jul/2016:01:40:14 +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 200 10355 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
***** - - [30/Jul/2016:01:40:14 +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 200 10355 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
***** - - [30/Jul/2016:01:40:15 +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 200 10355 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
***** - - [30/Jul/2016:01:40:15 +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 429★ 275 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
***** - - [30/Jul/2016:01:40:15 +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 429 275 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
***** - - [30/Jul/2016:01:40:15 +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 429 275 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
***** - - [30/Jul/2016:01:40:32★ +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 200★ 10355 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
***** - - [30/Jul/2016:01:40:33 +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 200 10355 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
***** - - [30/Jul/2016:01:40:33 +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 200 10355 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
***** - - [30/Jul/2016:01:40:33 +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 200 10355 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
***** - - [30/Jul/2016:01:40:33 +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 200 10355 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
***** - - [30/Jul/2016:01:40:33 +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 429 275 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
***** - - [30/Jul/2016:01:40:33 +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 429 275 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
6

6件目から429が帰ってるのがわかります。
また制限が10秒のうちに5回なので、しばらくまってリクエストして制限が解除されているのもわかります。
割と簡単だと思います。
ちなみに、1keyあたり100byteのメモリを使うのと、mutexを使用しているのでそこは頭の片隅に入れておくと良いかと思います。
といってもmutexは16分割してるので、通常の利用であればそこまで気にする程ではないと思います。


※ここから先は特にそのサイトのワークロードによってかなり考え方が変わります。
 なのでそのまま適用するのではなく、それぞれのサイトで考えてもらえればよいかと思います。
 また、異常行動に対してのキャップなので、継続的な改善サイクルを回す必要があります。
 そういうのは最初に想定仕切るの難しかったり、ページによってコストも違うからです。

とりあえずこれでスロットル早速やってみよう!という人がいましたらちょっと注意が必要です。
スロットルをかけるということは、ユーザの体験を制限してしまう可能性があります。
そのため安易に設定し、誤爆して普通の使い方をしているユーザを制限してしまわないように細心の注意を払う必要があります。
とはいっても、それでゆるゆるな制限をかけても全く有効ではありません。
個人的にスロットルをする上では2つのことが重要だと考えています。

  • 誤爆しない
  • 理不尽でない

あまりにも厳しく、普通に使っているユーザが誤爆されるような低い閾値であればイライラして離れていってしまうでしょう。
また、仮に引っかかっても、まぁしょうがないなとある程度納得ができるのも重要です。
例えば連打が許容されるボタンがあったとして、それを10秒で100回も連打して引っかかったら、まぁしょうがないなと思えるんじゃないかなと思います。
この辺りは、サイトの特性によって違いますので各々で考える必要があります。

誤爆しないための工夫
閾値の設定はすごく難しいです。
サイトの保護を優先したければ閾値は低くしたいですし
誤爆を防ぐには閾値は高くしたいです。

しかし、低くすれば誤爆が増え、高くすれば意味がなくなります。
ではどうすればよいかというと、トークンバケットへの理解とユーザの行動を考えてみることです。

最初にトークンバケットですが、これを何かに例えるとするとSuicaのチャージです。
飲み物や昼食などのお小遣いとして毎朝1000円チャージするとします。
チャージされてる分は自由に使って良くて、例えば一気に5000円使っても問題はありません(残額があれば)
また、Suicaのチャージ上限は2万円なので、そこに達した時点でそれ以上チャージができなくなります。
さらに開始時点から上限の2万円がチャージされているとします。
これをvsthrottle.is_deniedで表現すると


vsthrottle.is_denied(client.ip, 20000, 20d);

といった感じです。
平均すると1日辺り1000円使えますが、もし毎日2000円使うとなると20日経過した時点で初期にチャージされてた2万を使いきって一日待たないと2000円は使えなくなります。
1000円を超えてる部分はburstといった感じになります。

次に、ユーザの行動をYahooのトップ/ニュースページで考えてみましょう。(記事ページを守る)
ニュースの欄には複数のリンクがあって複数気になる記事があります。
その場合、行動は2パタンに別れると思います。

  1. 開いて読んで、読み終わったら戻って次の記事を読む
  2. 気になる記事を一気に開いて順次読んでいく

1は特に気にする必要はありません。記事を1秒で読んで素早く戻って次の記事を見ることができる人類はまずいないでしょう。
もし、1だけを考えるのであれば1秒に1回で制限をかけてしまえば良いと思います。
しかし、2の場合は一気にリンクを開くため、1秒で2~3は開くことは人類でもできるので先ほどのの制限では誤爆してしまいます。(本筋ではないですがChromeがbackspaceで戻れなくなったのでこういう人増えそうですね)
では、次に制限したいものを考えてみましょう。
今回はすべての記事を舐めてくようなクローラーで想定してみます。
このクローラーは3RPSで舐めていきます。
とりあえず登場するものが揃ったので条件を整理します。

1. 1ページ開いて5秒程度で読んで戻ってまた開くユーザの場合

  • avg=0.2RPS
  • burst=1RPS

2. リンクを最大6リンク程度開いて(秒間3程度)各ページを5秒程度で読むユーザの場合

  • avg=0.1875RPS
  • burst=3RPS

3. クローラー

  • avg=3RPS
  • burst=3RPS

条件が揃いました。
許容したいburstは2の3RPSです。
しかしavgでは0.2もあれば十分です。
これを単純に落としこむと
limit=3 period=15s (avg=0.2 burst=3)
となります。
しかし制限したいクローラーに対してはまだ余裕があるので自分ならですが
limit=4 period=10s (avg=0.4 burst=4)
ぐらいに設定すると思います。

とりあえずVCLを書いてみます。


vcl 4.0;
import vsthrottle;
import std;

sub vcl_recv {
  if(req.url ~ "^/pickup/" && vsthrottle.is_denied("pickup:" + client.ip, 4, 10s)) {
    std.log("THROTTLE:pickup:");
    //return (synth(429, "Too Many Requests"));
  }
}

ここでは一旦429は出さずにlogに出力しています。
これは実際に投入した後に、どういうクライアントが引っかかるかを試すためです。(テストのため引っ掛けるルールをちょろっと変えてます)


xcir@gw01:~$ sudo varnishncsa -q "vcl_log ~ 'THROTTLE:pickup:'"
***** - - [30/Jul/2016:19:48:38 +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 200 10355 "http://blog.xcir.net/?p=2283" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
***** - - [30/Jul/2016:19:48:38 +0900] "GET http://xcir.net/img/bg2.png HTTP/1.1" 200 10355 "http://blog.xcir.net/?p=2283" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"

vslqueryを使用して引っかかるものだけを確認して、問題がなければreturnのコメントを外して有効にしましょう。
個人的には、std.logはそのまま残しておいたほうがqueryで絞込ができるので便利だと思います。

IPは本当に個人を識別するものか
先ほどの例では、クライアントを識別するのにclient.ipを利用しました。
しかし、これは本当に個人を識別できているでしょうか?
例えば、アクセスログを見てるとHost名がgoogle-proxy-*.google.comというホストでアクセスしてくるクライアントがいます。
これはChromeのデータセーバーで一旦Googleを経由しています。(参考1, 2
つまり、同時に同一proxyから異なるユーザがリクエストしてくる可能性があります。
その場合はスロットルが意図せず掛かる可能性があります。
もちろん、Chromeのデータセーバー以外にもこのようなproxyがあります。
サイトの規模にもよりますが、大規模な場合はこの辺りも意識する必要があります。(リクエストが多ければ多いほどかぶる可能性が上がるので)
じゃぁキーをどうすればよいのかというといろいろあります。
ログイン後のページで制限をかけたいのであれば、クッキーからキーを抜き出しても良いと思います。
非ログインの場合は、ブラウザフィンガープリントを調べると良いかなと思います。(参考1, 2
他にもいろいろありますが、制限をしたい箇所によって最適な方法を模索していくと良いかなと思います。

最後に
スロットルはサイトでユーザがどう行動するかや、何が異常でそうでないかの見極めをきちんとしないとユーザ・運営共に酷いことになってしまいます。
しかしうまく使うことができれば、サイトの安定性を向上させることが出来ますので一度検討してみてはいかがでしょうか?


7月 132016
 

Varnish4.1.3がリリースされました。
バグフィックスが主でアップデートを強く進めますが、他にも結構使える新機能が増えています。

ダウンロードはこちら
changes

新機能・変更

varnishncsaにバックエンドに問い合わせにいったものだけを表示するオプションが追加されました(-b)
今までは幾つか絞込の指定をしないと取れなかったのですが-bだけで済むので便利です
また同時にクライアントで絞り込んで表示するオプション(-c)も追加されています。(デフォルト動作)
ちなみに-b -cを同時に指定することも出来ます。

VSMを開放する際にどれだけ待機するかのパラメータが追加されました(vsm_free_cooldown)
今までは60秒でハードコードされていました。デフォルト値も60秒です。
通常の使い方ではいじることはないと思いますが、かなり激しいログの書き込みをするとかの環境だといじる機会はあるかもしれないです。

varnishlogの出力でバックエンドのトランザクションが開始する際にBackendStartが入るようになりました
クライアントのReqStartと対になるものです。

varnishncsaの-Fで指定できるものが増えました
3つ増えました。
 Varnish:side
 先ほど紹介した-b -cオプションに伴って増えたものです。
 バックエンドで絞込をしている場合はbを、クライアントからの表示の場合はcを出力します。
 -b -cを両方指定している場合に使うとどちらのログなのかがわかって良いと思います。
 Varnish:vxid
 vxidを出力します
 VSL:tag VSL:tag[field]
 指定したタグ(TimeStampなど)を出力します。
 基本vsl-queryと同じ指定ですがヘッダ指定は出来ません(VSL:ReqHeader:User-Agentみたいなのは出来ない)
 これできるといいなーと思うのでp-r書こうかな・・
 ちなみに複数引っかかる場合は最初のを出力します。
 例えばVSL:TimeStampを指定した場合は毎回Startのタイムスタンプが表示されます

TCP Fast Openをサポートしました
デフォルトはoffです(tcp_fastopen)

varnishtestに新しい同期用の命令を追加しました(barriers)
semaより使いやすいです

varnishstatで12桁以上の数値を出力する場合は丸めるようにしました#1855
CURRENTの表示でK/M/G/Tのように単位がつけられないカウンタは” %12ju”だったのですが12桁を超える場合は1000で割って” %9ju…”という表示になります。
コードを見た限りではxml/jsonで出力するときには影響しないのでそこまで気にすることはないかなと思います。

バグ修正

今回はかなりバグ修正が多いので幾つかピックアップして紹介します。
特にESI周りの修正が多いのでESIを使っている場合はアップデートすると良いと思います。

varnishncsaで-Lオプションが受け付けられないようになってたのを修正しました#1994

たまにAgeとAccept-Rangesヘッダが複数レスポンスされることがあるのを修正しました#1955

同名のVCLでvarnishをstop->startを繰り返すとその後segfaultを繰り返す事があったのを修正しました。#1933
dlopenのリファレンスカウンタが信用ならなかったみたいです。
vcl名は変わらないのですが、コンパイルしたvgc.soのパスに現在時刻(ナノ秒)が付与されるようになりました。
バッドアイデアとメッセージにあるので割と苦悩した感じがあります。

vcl_init/finiでstd.log/std.syslogを利用するとクラッシュするのを修正しました#1924

VSMのサイズが小さくてオーバーラン検出に問題が合ったのを修正しました#1873
varnishncsa等でオーバーランした際にもクラッシュしなくなりますが
オーバーランしたらvsmサイズを増やしたほうが良いでしょう。
それでも困るようであれば今回追加されたcooldownの調整も考えると良いかも

-Cオプション利用時にテンポラリで使用したディレクトリを消していなかったのを消すようにしました#1869

POSTリクエストをpipeで繋いだ場合に1分待たされることがあるのを修正しました#1806
4.1からはPOSTリクエストのデフォルトの動作がpassになっているので通常の場合は問題ないのですが
以前からの設定を使いまわしてる人などは割りとハマるかもしれないです。

バックエンドの接続数を示すカウンタを復活させました#1725
バックエンド周りのコード修正した際に意図せず消えたカウンタを復活させました。
確かに消えてました・・気づかなかった・・

次は5.0.0かなー
楽しみですね