xcir

\いわなちゃん/

4月 092018
 

Varnish6.0.0がリリースされました。[changelog] [公式のリリース文] [パッケージDL] [ソースDL]
今回のリリースはメジャーバージョンアップということで機能追加や累積バグFixなど来ています。
目玉機能はUnixDomainSocket(UDS)対応で、個人的に注目しているのがshard directorにおけるlazyモード追加です。
また新しいVCLバージョン(4.1)も追加されていますが、既存が使えなくなったわけではないので影響は少ないでしょう。
なお、アップデートする場合はいくつか注意が必要な点があります。

現状で適用する場合は十分に注意検討する必要がある環境

http/2環境
h2利用時にスレッドがリークしているような動きをしています。(#2623)
最初は自分のテスト用環境だけかなと考えていたんですが、他に踏んでる人を確認したので現状のままでh2を有効にするのはかなり注意が必要です。
h2を使いたいのであれば、きちんと検証して自環境で起きないことを確認しつつ行うか、既存バージョンのままで使いつつFixを待つと良いと思います。
ただしh1のみの場合は現状問題ないため、h2を使わないのであればバージョンアップをしても良いかと思います。

公式パッケージの対象バージョンが少なくなった
公式で提供してるパッケージの対象OSは以下の通りです
– RHEL7(CentOS7)
– Debian9
– Ubuntu16.04
Ubuntu14.04やRHEL6は対象外となりましたので注意が必要です。
ただ、これはあくまで対象バージョンを絞ったというだけなので、自前でpkg-varnish-cacheを利用してパッケージを作って適用は可能です。

VMODを利用している場合
VMOD側で対応しないといけない変更が入っているため、いろんなVMODを利用している場合は対応状況を確認したほうが良いです。
必要な変更はたいしたことないので、メンテされてるものについては対応されるんじゃないかなとか思います。

なお、VSMに直接つなぐアプリについては5.2.0のときのVSM/VUTインタフェース変更に対応できていればほぼ動くと思います。
一部定数と戻り値の型が変わったものがあるためそこを使っていて運が悪いと踏むかもしれません。


VCL

vclを記述する際には、先頭に「vcl 4.0;」と記述していたかと思いますが、今回「vcl 4.1;」が追加されました。
既存の4.0が使えなくなったわけではないため、そのまま動かすことも可能ですし、大抵の場合は先頭行だけ4.1に書き換えても(大抵の場合は)動くでしょう。
あとで紹介するUDSは4.1のみでサポートしている記法が必要なので、もし使う場合は4.1に変更しましょう。
またincludeですが親のVCLバージョンより小さい場合は可能です。
つまり

parent vcl ver child vcl ver result
4.0 4.0 OK
4.0 4.1 NG
4.1 4.0 OK
4.1 4.1 OK

こんな感じです。
VCLの記載の仕方は環境それぞれだとおもうのでなんともいえないのですが、
default.vclを起点として、includeしていくやり方であればdefault.vclを一旦4.1にしておくと良いのかなと思います。
とはいえ、4.0から4.1へのアップデートはそんなに難しくないのでやってしまったほうがいいんじゃないかなとは思います。

VCL変更点

4.0での変更点と4.1での変更点があります。
まずは共通の変更点から

変更点(4.0/4.1共通)

return(fetch)がvcl_hitで使えなくなりました
個人的にはまだ残っていたのかといった感じですが(結構前からdeplicatedになってた)
return(miss)を使いましょう

req.storage / hash_ignore_busy / hash_always_missをclientスレッド側でアクセスできるようになりました。

obj.storageが追加され、vcl_hit/vcl_deliverでアクセスできるようになりました

restart時の動作が変わりました
req.restarts,xid以外のすべてのreq変数は維持されます。
restartを利用している場合は動作の確認をしたほうが良いでしょう。

vcl_recvでもreturn(restart)が呼べるようになりました

変更点(4.1のみ)

backendの定義に.pathが追加
あとで触れるUDSで使うパスです。
.hostと同時に定義することは出来ません。

local.socketの追加
接続に利用したソケットの名前を返します。
-afoo=:81と名前が指定されてる場合はfooを返しますが、もし定義されていない場合は自動的につけられた名前(a0,a1)が返却されます。

local.endpointの追加
接続に利用したソケットの定義を返します。
-afoo=:81の場合は:81が返却されます

sess.xidの追加
セッションのユニークなID(xid)を返します。
今まで使用していたreq.xid/bereq.xidはそのリクエスト、バックエンドリクエストを示すものでした。
しかしKeepaliveやHTTP/2では1セッション中に複数のリクエストが含まれます。


*   << Session  >> 433160262
-   Begin          sess 0 PROXY
-   SessOpen       ***
-   Proxy          ***
-   Link           req 423002616 rxreq
-   Link           req 423002617 rxreq
-   Link           req 423002618 rxreq
-   Link           req 423002619 rxreq
-   Link           req 423002620 rxreq
-   ReqAcct        72 22 94 1251 6 1257
-   End

これはとあるHTTP/2.0でのログの抜粋ですが、これが示しているのは
xid:433160262のセッション(sess.xid)にxid:423002616~423002620のリクエスト(req.xid)がぶら下がっているということです。
今回sess.xidがvcl中から簡単に取れることで、異なるリクエスト中でも同一セッションであることがわかるので、なにかに使えるかもしれないです(ぱっと思いつかないですが・・)

resp.do_esiの追加
beresp.do_esiの指定でフェッチ時にパースした場合でも無効にすることができます。
vcl_deliver/vcl_synthで呼び出し可能です。

(req|bereq|beresp).protoがreadonly
そんなに変更することもなかったと思いますが.protoがreadonlyになりました。

req.esiが削除された
まぁ使う人はそんなにいないかと・・・beresp.do_esiを使いましょう

beresp.storage_hintが削除されました
beresp.storageで代用できます。
例えば今まで


set beresp.storage_hint = "foo";

としていたのであれば


set beresp.storage = storage.foo;

と変更すればよいです。

beresp.backend.ipがが削除されました

IPアドレスが0.0.0.0:0のものはUDS経由のものになります。


Unix Domain Socket(UDS)対応

上下流どちらも対応しています。
これを利用することにより高CPS環境において、特に同一サーバに置いてることがほとんどだと思われるTLS終端を行うミドルウェアとの通信をより効率的に行えるようになります。(上流側)

なお、今回の例ではNginxを組み合わせてやってみます。

LISTEN側でUDSを使う場合
起動パラメータの-aで以下のような指定をします


-a /var/run/varnish-uds.sock,user=vcache,group=varnish,mode=666

今回はproxyプロトコルを指定していませんが指定できます。(PROXYを追加しましょう)
また今回はお手軽にNginxとつなぐために666にしていますが、適切な設定にするべきでしょう。

Nginx側


upstream backend {
    server unix:/var/run/varnish-uds.sock;
}
server {
        listen 80 default_server;
        listen [::]:80 default_server;
        location / {
            proxy_pass http://backend;
        }
...
}

これだけです。
なお、LISTEN側でUDSを使う場合はVCL4.1が必須です。
が、現状試してる限りでは起点となるvcl(default.vcl)が4.1であればinclude先は4.0でも平気な動きをしています。 
しかし、ドキュメント上は

When UDS listeners are in use, VCL >= 4.1 will be required for all VCL programs loaded by Varnish.

と記述されており、もしかしたら塞がれる可能性もありますので、UDSを使う場合はすべてのVCLを4.1にするのが無難でしょう。

なお、UDSは当然ながらIPアドレスはなく、UDS経由のアクセスは0.0.0.0:0として扱われます。
なので、client.identity(デフォルトではclient.ipが使われる)を使って振り分けを行う場合は常に同一backendが選択されるので注意が必要です。
適切に修正を入れるか、そもそも上流はPROXYプロトコルが使えるミドルウェアにするなどの対策が必要でしょう。

バックエンド側でUDSを使う場合
すごく単純です
Nginx側


server {
  listen unix:/var/run/nginx-uds.sock;
  access_log off;
  root /var/www/html;
  index index.html index.htm index.nginx-debian.html;
}

Varnish側


vcl 4.1;
backend default {
    .path = "/var/run/nginx-uds.sock";//絶対パス
}

これだけです。
特に難しくないと思いますが、1点注意なのがbackend内での.pathプロパティはvcl4.1のみなので4.0だと出来ません。
なおListenの時と違いバックエンドのみ使用する場合は4.0混在でも動きます。とはいえ4.1の書き換えは難しくないので書き換えたほうが無難でしょう。


その他新機能・変更

varnishncsa

制御文字・バイナリを含む可能性があるタグの-Fを拒否するようになりました
対象はH2RxHdr H2RxBody H2TxHdr H2TxBody Debug HttpGarbage Hashですが、たぶん使ってる人はいないかなと

起動パラメータの変更

esi_iovsの追加
ESI処理で使うiovec構造体の割り当て数ですが通常の場合はいじる必要がありません。
ESIを使っていてどうしてもtuneしたい場合はsystemcallでwritevの実使用数をみながら調整するとよいでしょう
またこの値を変更する場合はworkspace_threadを同時に調整する必要があるでしょう(割り当て元がworkspace_threadからなので)

h2関連パラメータの追加
h2_header_table_size
h2_max_concurrent_streams
h2_initial_window_size
h2_max_frame_size
h2_max_header_list_size
このあたりはh2のRFCを読んでもらえればわかるかなって感じです。
ちなみにh2_max_concurrent_streamsは100なんですが、chromeのストリームはデフォで1000なので、そのあたりで気になる人もいるかもしれないです。(とはいえそんなに弄る必要ないと思います)

feature_bitの追加(http_dete_postel)

日付の形式はrfc7231#section-7.1.1.2で定義されてるんですが、たまにこれに準拠しないやんちゃなDate,Last-Modified,Expiresを返してくるサーバがいます


Fri, 2 Mar 2018 14:26:02 GMT  (まちがい)
Fri, 02 Mar 2018 14:26:02 GMT (ただしい)

この間違いのパタンを許容するようになります
有効にしたい場合は-p feature=+http_dete_postelにします。
個人的にはオリジン側直したほうがよいかなーとは思いますが・・・

cli_bufferの削除
まぁ以前からアナウンスあったので・・・

カウンタの追加

cache_hit_grace
そのまんまですがgrace状態でレスポンスできた件数になります。
パフォーマンスを稼ぐ場合は一旦古い状態のキャッシュを返しつつ裏で更新リクエストを行うのを重視しますが(うまくいけばキャッシュ更新時に遅くならない)
このカウンタを見てチューニングを行うことが出来るかなとも思います。 

n_lru_limited
nuke_limitに達した件数を示します。
当然ですがストレージがいっぱいの状態で新しいオブジェクトをキャッシュを行おうとする場合は古いものを押し出す必要があります。(LRUです)
Varnishではこの処理をNukeと呼んでいますが、実はこの処理にはnuke_limitというパラメータがあり、一つのオブジェクトを確保するためにNukeして良いオブジェクトのLimitを定義しています。
オブジェクトサイズがだいたい似たようなところならデフォルト値(50)でも問題ないのですが、平均50KBのところに10MBのでかいオブジェクトがたまにやってくるとかだと足りないことがあります。
当然Limitを超えた場合はストレージを確保できないためエラー(503)となるのですが、問題はストリームをしている場合です。
ストリームでレスポンスをしている場合は、当然ながらオリジンから帰ってきたレスポンスコード(200)を返してしまっているので503を返すことができません。
そのため200なのにレスポンスボディが中途半端なサイズになるということがあるのですが(割とmlとか見てるとそれっぽい問い合わせ多い)
このカウンタを見ることでそのような状態に陥ってることがわかります。
とりあえず1でもあがるようであれば、nuke_limitを引き上げる、ストレージの見直しをする(サイズあげるとか、ある程度のサイズ毎に分けてしまうとか)とかすると良いと思います。

vmod_directors

shard directorにかなりの修正が入っています。
shard_param
新しくshard_paramというクラスが定義されています。
基本的に今までのshardの各種パラメータ(rampupとか)が定義されており、単純に外出しにされたものです
使い方は


sub vcl_init{
	new vd = directors.shard();
	vd.add_backend(hoge);
	new p = directors.shard_param();
	p.set(by=URL, warmup=0.7);
	vd.associate(p.use());
}


sub vcl_init{
	new vd = directors.shard();
	vd.add_backend(hoge);
	new p = directors.shard_param();
	p.set(by=URL, warmup=0.7);
}
sub vcl_backend_fetch{
	set bereq.backend=vd.backend(param=p);
}

こんな感じです。

ハッシュアルゴリズムについて
今までハッシュアルゴリズムを選択できたのですが(SHA256(default),CRC32,RS)
これがSHA256だけになりました。これに伴いalgを指定できていたメソッドにおいてalg引数が削除されました。
しかし、わざわざSHA256以外を使ってる人も少ないと思うのでそこまで影響ないと思います。
引数がなくなる影響を受けるのは.reconfigureと.keyになります。

resolve={now,lazy}の追加
個人的に注目している機能です。
lazyモードは実際にバックエンドへの接続を作るタイミングで解決されるものです。
これが何が嬉しいのかというと、Directorを入れ子にすることができます。
例えばfallbackと組み合わせてdirectorが全滅したらfallbackするみたいなのも書けます。


probe healthcheck {
    .url ="/check";
    .timeout           = 1s;
    .window            = 4;
    .threshold         = 2;
    .interval          = 1s;
}
backend ws01  {.port="80";.host = "XX";.probe=healthcheck;}
backend ws02  {.port="80";.host = "YY";.probe=healthcheck;}

sub vcl_init {
	new sdp = directors.shard_param();
	sdp.set(warmup=0.5, healthy=ALL);

	new sd1 = directors.shard();
	sd1.associate(sdp.use());
	sd1.add_backend(ws01);
	sd1.reconfigure();

	new sd2 = directors.shard();
	sd2.associate(sdp.use());
	sd2.add_backend(ws02);
	sd2.reconfigure();

	new fb = directors.fallback();
	fb.add_backend(sd1.backend());//lazy
	fb.add_backend(sd2.backend());//lazy

}
sub vcl_recv {
	set req.backend_hint = fb.backend();
	return(pass);
}

サンプルでshardに登録してるバックエンドはそれぞれ一つですが感覚は掴んでもらえるかと思います。(ついでにshard_paramもサンプル的に入れてみました)

vmod_std

std.log/syslogにおいてworkspace枯渇した場合でもfailしないようになりました

vmod_unix

今回追加されたvmodです
接続してきたプロセスのuser/group/uid/gidを取得できます。
厳密にガードをかけたいときに使うとよいかもしれないです。
man vmod_unixそのままですが


sub vcl_recv {
      # Return "403 Forbidden" if the connected peer is
      # not running as the user "trusteduser".
      if (unix.user() != "trusteduser") {
              return( synth(403) );
      }

      # Require the connected peer to run in the group
      # "trustedgroup".
      if (unix.group() != "trustedgroup") {
              return( synth(403) );
      }

      # Require the connected peer to run under a specific numeric
      # user id.
      if (unix.uid() != 4711) {
              return( synth(403) );
      }

      # Require the connected peer to run under a numeric group id.
      if (unix.gid() != 815) {
              return( synth(403) );
      }
}

こんな感じです

vmod_proxy

今回追加されたvmodです
Proxy Protocol v2にはType-Length-Value(TLV)というフィールドに例えばどんなCipherを使用しているかみたいな情報を埋め込むことが可能です。
残念ながらHitchはalpn(PP2_TYPE_ALPN)しか埋め込んで来ないようなので、フル機能を使いたい場合はHAProxyの比較的新しいバージョンを使う必要がありますが
何かしら情報を取得してログに出力したい場合は有用だと考えています。
また、残念ながら取れる値は固定になっているので、カスタムTLVの範囲へは現状アクセスできません。(NLBのVPCエンドポイントIDとかl)
とはいえコードを見る限りでは対応は容易なのでp-rしてみるのもおもしろいかもしれないです。
使い方は単純で


vcl 4.1;
import proxy;
import std;
sub vcl_recv{
	if(proxy.alpn() == "http/1.1"){
		//プロトコルはまぁreq.protoで取れるんで適切ではないかもですがサンプルとして
		...
	}
}

こんな感じです

VSL

ReqAcctのバイト数が正確になりました
プロトコルのオーバーヘッド分(chunkedのとか)は今まで入ってませんでしたが、OSから報告される値を使うように変更したので正確になりました。

バグ修正

多くのバグ修正がされているのですが、その中でも特筆したほうがよいものに触れます。
max_restartsの取扱い
return(restart)を行った際のチェックの不備で、max_restrtsで指定した回数より1回少ないrestartでlimitになっていましたが、修正されてmax_restartsの指定した数return(restart)ができるようになりました。


11月 222017
 

最近のVarnishのリリースサイクルは半年に一回で、その間はセキュリティ等のクリティカルな問題がない限りはリリースされません。
当然、半年の間に様々なFeatureやBugfixなどがコミットされており、場合によっては使いたいこともあるかと思います。
今までにそのように最新のVarnishを使うためには、以前紹介したpkg-varnishcacheを利用して自前でビルドする必要がありましたが
それなりに敷居が高く、そんなに使ってる人はいないんじゃないかと思います。
しかし、今月に入ってからweekly-buildがされるようになりました。(rpm/deb)

https://packagecloud.io/varnishcache/varnish-weekly

branchはtrunkになりますので、場合によってはすでに互換性のない変更が上がっている可能性がありますが
気になるfeatureやBugfixなどがあって更新したいときは使ってみるとよいかと思います。
なお、weeklyとありますが、基本的に毎週月曜に行っているbug-washでビルドするか決めているので
開発中のfeatureがあるなどの場合はskipされるみたいです。


11月 162017
 

Varnishの複数のバージョンがリリースされました。 [changelog] [パッケージDL] [ソースDL]

今回のリリースはセキュリティ対策によるもので、なるだけ迅速にアップデート、もしくは回避するために設定を見直す必要があります。
詳しくは公式情報(VSV00002)を参照してください。

脆弱性の内容

以下の条件を満たす際にsegfault/データリークが発生する可能性があります。

  • VCL_backend_errorで生成したsyntheticオブジェクト(beresp.body)を
  • file/persistentストレージに挿入する
原因

バックエンドへの接続が失敗したりなどで表示されるエラー画面はvcl_backend_errorで生成されています。
builtin.vclをみればわかるようにこのアクション内でエラー画面を生成しています。
で、この生成したものを後処理でストレージを確保してからberesp.bodyからストレージへmemcpy(3)するんですが、この際にfile/persistentストレージがターゲットに選ばれた際に問題が発生します。
file/persistentストレージの領域はpage-sizeでalignされており、確保サイズが割り切れない場合はちょっと余分な容量がついてきます。
で、この時にmemcpy(3)の第三引数に指定するサイズをalignされたサイズをそのまま渡してしまったために、コピー元から余分なサイズをreadしようとしてしまいます(なのでだいたいはsegfaultするかと)

では、vcl_backend_errorを通過すると必ず起きるかというとそうでもないです。
このようなエラー画面などは、TTLが短いためtransientというテンポラリストレージに保存するようになっています。
これは明示的に指定していない場合はmallocストレージエンジンが利用されるので今回のケースには当てはまりません。
しかし、transientに挿入する条件はエラー画面だからとかそういうのではなく、純粋にTTL(正確にはttl+grace+keepの合計値)がパラメータのshortlived未満かどうかです。
shortlivedを超える場合はhintで指定された(無指定はdefault)ストレージを使用します。
そして、shortlivedはデフォルトで10秒なので、VCL_backend_errorにおいてttlがそれ以上になるようにしていると引っかかります。(そのような人は珍しい気もしますが・・)
では、そのまま使っているケースであれば踏まないかというと、実は踏むケースが存在します。
ここのコードを見てみると同一リクエストがある状態(waitinglistに入ってる)の場合だとttl+grace+keepの合計が11secになります。
デフォルトのshortlivedが10secなのでここにタッチしますので、この状態でdefault storageがfile/persistentを使用している場合は踏んでしまいます。
なのでまとめるとこんな感じになります

Transientを明示的にfile/persistent指定している場合(ex -sTransient=file…)
vcl_backend_errorに突入した場合は引っかかります。
そもそもTransientをfile/persistentで使うメリットはそこまで無いので再検討するとよいかと思います。

デフォルトストレージにfile/persistent指定している場合
このケースでは2パタンあります。
vcl_backend_errorでTTL/grace/keepを明示的に指定していて、合計値がshortlived以上の場合はvcl_backend_errorに突入した場合は引っかかります。
次にTTLの変更もなく、shortlivedの変更もない(default 10sec)場合で同一リクエストがラッシュしてる状態だとTTL合計値がshortlivedを超えるためvcl_backend_errorに突入した場合は引っかかります。

なお、同様にsyntheticオブジェクトを生成するvcl_synthのほうは影響を受けません。
vcl_synthの場合はここでmemcpy(3)していますが、resp.bodyのlength(szl)を利用していますので問題ありません。

暫定の回避方法

それぞれの設定によって違いますが、説明したとおりとりあえずvcl_backend_errorでfile/persistentストレージが使われなければOKです。
なのでvcl_backend_errorもshortlivedもいじってない場合はshortlivedを12sに設定するかvcl_backend_errorでのTTL/grace/keep指定を明示的に0にすると良いでしょう。
なお、shortlivedを引き上げた場合はメモリの使用状況が変わるのでモニタリングする必要がありますし、TTLを明示的に0にした場合は若干ラッシュ時のオリジンの耐性が弱くなるでしょう。(気にするレベルではないと思います)
まぁどちらにせよ早めにアップデートするのが良いでしょう。

そのほかの変更について

今回はこの脆弱性以外にもいくつか修正があります。

4.1.9/5.2.1共通の変更
バグ修正

クライアントからのリクエストが空っぽの場合にidle_timeoutが正しく使用されなかったのを修正(#2492)

4.1.9のみの変更

基本的に5.2.0にあるもののbackportになります

新機能

bereq.is_bgfetchの追加
req.ttlの評価変更(#2422)

バグ修正

いくつかのカウンタの修正


9月 242017
 

VarnishCache5.2.0がリリースされました。 [changelog] [公式のリリース文] [パッケージDL] [ソースDL]
バグの修正(特にh2関連で多くの修正入っています)はもちろんですが、
新しいvmodが提供されたり、影響の大きい(破壊的)変更があります。
また、VMOD/TOOL開発者向けの変更も多く、新しいツールなどが期待できます。

影響の大きい変更

VSMの抜本的な変更

Varnishの内部でログや統計の保存先に使用しているVSMの仕組みが変わりました。
これによりVSMを利用してログ(VSL)や統計(VSC)にアクセスしているサードパーティのプログラムについては対応が必要になります。
なお、VSMのラッパーであるVUTについても変更があるのでこれ関係は全部修正が必要と考えても差し支えありません。
(Varnish UTilities:VUTはVSMを使う上でlog,ncsa等のコマンドで毎回同じコードを書くのが面倒でまとめてる高レベルAPIと思ってもらえれば間違いないです)

例えば影響を受けるところとしては
vago
python-varnishapi
varnishkafka
などがあります。
そのためバージョンアップを行う場合は、特にログ、監視周りについて正常に動作するかなどの確認を行う必要があります。

varnishstatの出力フォーマットの変更(-j/-x)

例えばmuninなどの監視ミドルウェアの一部はVarnish公式コマンド群のvarnishstatを叩いて統計を取得しているケースがあります。
今回varnishstatの-j(json)-x(xml)オプションの出力からtype, identフィールドが削除されました。


~5.2.0(-j)
  "MAIN.uptime": {
    "description": "Child process uptime",
    "type": "MAIN", "flag": "c", "format": "d",
    "value": 4043809
  },

5.2.0~(-j)
  "MGT.uptime": {
    "description": "Management process uptime",
    "flag": "c", "format": "d",
    "value": 168192
  },

そのためそのフィールドを利用している場合は動かなくなる可能性があります。
自分が確認した限りだとmuninのvarnish4プラグインについては影響を受け、修正が必要になります。

一部VSLタグのフォーマット変更

これも主にツールを作ってる人たち向けですが(もしくはvarnishlogで生ログみてる人)
一部VSLタグのフォーマット変更されており、そのためパースに失敗するなど起こる可能性があります。

Hit, HitMiss, HitPass
今まではVXIDのみでしたが、追加で残TTLを出力するようになりました。(Hitについてはgrace/keepTTLも出力)

SessOpen
第3フィールドには今までlistenソケット(-aオプションの値)がそのまま表示されていましたが、
このバージョンからは-aで指定された名前が出力されます。指定されていない場合はvarnish側で自動でつけられた名前(a0など)が入ります。(後述します)

VCL_trace
新規にVCLの名前とsource indexを出力するようになりました。
これにより、今まではVCLのどこを通過したかを判断するのが割とめんどくさかったのが簡単にできるようになります


10 VCL_trace      c boot 4 0.23.3
                    |    | | |  |
                    |    | | |  +- VCL program line position
                    |    | | +---- VCL program line number
                    |    | +------ VCL program source index
                    |    +-------- VCL trace point index
                    +------------- VCL configname

例えばこの出力の場合であれば


# varnishadm vcl.show -v boot

で出てきたVCLから


// VCL.SHOW 0 939 /etc/varnish/default2.vcl
#           ↑ここがsource indexになる
#
# This is an example VCL file for Varnish.
...

の記述を探して


 22 sub vcl_deliver{
 23   set resp.http.Hash = blob.encode(BASE64, blob=req.hash);
      ↑ここ

line:23 pos:3をさがします
これでどこを通過したかがわかります。

その他変更

varnishdの変更

-lでvsmサイズを指定する必要がなくなりました
先程も述べたとおりvsmの仕組みが変わった影響でサイズを指定する必要がなくなりました。
とりあえずは無視されるようになっています。

-aで名前が指定できるようになりました
listenソケットの名前を指定できるようになりました。(-a admin=127.0.0.1:88の場合だと名前がadminになる)
現時点では先述したSessOpenのフォーマットに影響する程度です。
次バージョンへの準備と思ってもらえれば良いかと。

varnishstatの変更

MAIN.s_reqは廃止されました
MAIN.client_reqと同じなのでこちらを見るようにしましょう

MAIN.req_droppedが追加されました
h2ストリームが拒否された回数を示します。

-Nオプションの廃止

varnishlog,stat,ncsa,histに存在した-Nオプションは廃止されました。

VCLの変更

変数の追加や一部変更はありますが、既存の修正が必要になるような変更はありません。基本的には5.1で動いていたVCLはそのまま動くと思います。

[動作変更]server.identity
以前は-nオプションの指定(def:インスタンス名)だったのですが、このバージョンより-iオプションの値になりました。
もし-iで指定していない場合はホスト名(gethostname(3)の結果)になります。

[追加](req|bereq).hash
現在のオブジェクトのキーハッシュをBLOB出力します。
後述するvmod_blobを利用することでHEXやBASE64形式に変換することが出来ます


sub vcl_deliver {
    # base64形式でハッシュをレスポンスヘッダにセットする
    # Hash: ANTj5yFpZGjJsL0IaxEDGiG29h0fSZRvjywdvDkKihc=
    # こんな感じで出力される
    set resp.http.Hash = blob.encode(BASE64, blob=req.hash);
}

[追加]bereq.is_bgfetch
Varnishのバックエンドへのフェッチは2つあり、キャッシュが無いなどでリクエストと同時に取得しにいくfetchとgraceの動作などでバックグラウンドでfetchを行うbgfetchが存在します。(ここのバックグラウンド更新について見てもらえればどんな動きするかわかると思います)
今まではbackend_fetchなどのバックエンドアクションにおいて、今処理しているfetchがバックグラウンド処理なのかを判定する方法はありませんでしたが
この変数を使うことによって可能になりました。
bgfetchを行っている場合はtrueが入ります。また読み取り専用なので書き込みは出来ません。

[その他]req.backend_hint
特に変更というわけではないと思うんですが
req.backend_hintのrestart時の挙動が明記されました。
デフォルトのバックエンドにリセットされます。

vmod_std

新しく指定ファイルの有無をtrue/falseで返すfile_existsが追加されました。
この関数はstd.filereadと違い結果はキャッシュされないので呼び出すごとにstatが呼び出されます。

vmod_blob

新しくvmod_blobが追加されました。
実はあまり使われていないのですがVCLにはBLOB型というのが存在しており、主にvmod間でのデータの受け渡しなどで使用されています。
このvmod_blobはencode/decodeなどを提供しています。
対応エンコードにはbase64/urlencodeも含んでおり便利かと思います。
とりあえず基本的な使い方について解説します。

例えばHEX表記されたfoo(666F6F00)を一旦blobに変換したあとにSTRINGに変換してヘッダに入れる場合はこうなります。


sub vcl_deliver{
  set resp.http.foo = blob.encode(IDENTITY, blob=blob.decode(HEX, encoded="666F6F00"));
}

ここでIDENTITYやHEXがフォーマットです
ちなみにblob.decode/encodeを使わずとも一気に変換するtranscodeというものもあります。
先程と同じ出力を得るには


sub vcl_deliver{
  set resp.http.foo = blob.transcode(encoding=IDENTITY, decoding=HEX, encoded="666F6F00");
}

でも出来ます。

とりあえず対応フォーマットです。

  • IDENTITY
  • BASE64
  • BASE64URL
  • BASE64URLNOPAD
  • HEX
  • URL

BASE64とかURL(URLエンコード)はみたまんまなので特に解説はいらないと思います。
IDENTITYフォーマットは特に何らかのエンコードがされているわけではなく入力された値そのままを使います。
なので先程の例で666F6F00を出力した場合はfooになったわけです。
ちなみにHEX形式の先頭には0xは不要ですので注意しましょう。

詳しい使い方についてはこちらを参照ください

vmod_purge

組み込みのpurgeをより柔軟に使うためのものです。
ちなみに既存のreturn(purge)がなくなったわけではありません。
なお以下の関数はすべてvcl_hit/vcl_missでのみ呼び出し可能です。

INT purge.hard()
return(purge)と同じ挙動をします。(今のハッシュと一致するオブジェクトを消す)
戻り値は消したオブジェクト数になります。

INT purge.soft(DURATION ttl=0, DURATION grace=-1, DURATION keep=-1)
デフォルトの場合はTTLを0にします。ただしgraceやkeepは変更しません(-1)
ちなみにhard()はsoft(0,0,0)と同じです。
まぁ、既にキャッシュされたオブジェクトのTTL,grace,keepを自由にいじるのに使えると思ってもらえれば良いかと思います。
なお戻り値は操作したオブジェクト数になります。

vmod_vtc

これは元々vmod作者がテストコードを記述する際に便利に使えるものです。
子プロセスをpanicさせたりする関数が含まれており、通常のユーザーが使うものではないと思います。

バグ修正

多くのバグが修正されているため、そこまで触れませんが1点だけ・・・
http/2においてメモリリークのバグが修正されています。
今のところ該当パッチが当たってから一月以上稼働させていますが、安定動作しております。
まだexperimentalなので人柱覚悟とはいえ普通に使えるんじゃないかと思います。

次のバージョンについて

次の大型リリースは2018年3月予定です。
実は元々今回はメジャーバージョンアップが予定されていたのですが、予定リリース日に間に合わないということで一部分を切り出して5.2でリリースされました。
なので既に次期バージョンの片鱗はprなどにあるのですが(例えばUDS対応)なかなかおもしろい機能が増えたバージョンになりそうで、個人的には楽しみにしてます。


8月 032017
 

Varnishの複数のバージョンがリリースされました。 [changelog] [パッケージDL] [ソースDL]

今回のリリースはセキュリティ対策によるもので、なるだけ迅速にアップデート、もしくは回避VCLを入れるべきです。
詳しくは公式情報(VSV00001)を参照してください。

脆弱性の内容

書くか少し迷ったんですが、テストコードがまんまコミットされてるので解説します・・
クライアントがTransfer-Encoding: chunkedかつchunk-sizeの指定が内部の変数でMSBが立つぐらい大きな指定でリクエストを行った場合、AssertでVarnishが再起動します。(DoS)


$ sudo varnishadm panic.show|head
Panic at: Wed, 02 Aug 2017 16:45:40 GMT
Assert error in v1f_pull_chunked(), http1/cache_http1_vfp.c line 172:
  Condition((vfe->priv2) == 0) not true.
version = varnish-5.1.2 revision 6ece695, vrt api = 6.
0
...

対象バージョン

Transfer-Encoding: chunkedに対応したのは4.0.1からなのでそれ以降のすべてのバージョンになります。

バージョン系 影響を受けるバージョン 修正バージョン パッケージDL先
4.0.x 4.0.1~4.0.4 4.0.5 link
4.1.x 4.1.0~4.1.7 4.1.8 link
5.0.x 5.0.0 5.1.3まで上げる必要があります link
5.1.x 5.1.0~5.1.2 5.1.3 link

4.0.0及び3.0.xは先に述べた通り、そもそも今回の機能がないので対象外です。

回避方法(VCL)

VCLでの暫定的な回避方法があります。
これらはchunkedでリクエストしてきたものを503で落とすことで回避します。
大抵のブラウザの場合はTransfer-Encodingを指定したリクエストは行わない事が多いのですが、念のため確認すると良いでしょう。
varnishlog -cq ReqHeader:Transfer-Encoding -i ReqMethod -i ReqURL
これで何かしら出てきて、正規のリクエストの場合はVCLでの回避は不可能ですのでバージョンを上げる必要があります。

なおここで紹介している回避コードは公式そのままなので公式も参考にしてください(VSV00001)
4.0.x向け
インラインCを利用します。
そのためvcc_allow_inline_cをtrueに設定する必要があります
起動パラメータの場合は
-pvcc_allow_inline_c=true
varnishadmで指定する場合は
varnishadm param.set vcc_allow_inline_c true
で可能です
VCLは以下の通りです。


sub exploit_workaround_4_0 {
        # This needs to come before your vcl_recv function
        # The following code is only valid for Varnish Cache and
        # Varnish Cache Plus versions 4.0.x
        if (req.http.transfer-encoding ~ "(?i)chunked") {
                C{
                struct dummy_req {
                        unsigned magic;
                        int restarts;
                        int esi_level;
                        int disable_esi;
                        char hash_ignore_busy;
                        char hash_always_miss;
                        void *sp;
                        void *wrk;
                        int req_step;
                        struct {
                                void *a;
                                void *b;
                        };
                        int req_body_status;
                };
                ((struct dummy_req *)ctx->req)->req_body_status = 6;
                }C

                return (synth(503, "Bad request"));
        }
}

sub vcl_recv {
        # Call this early in your vcl_recv function
        call exploit_workaround_4_0;
}

4.1.xと5.0.0向け
同じくインラインCを利用しますので4.0.xで有効にしたように指定が必要です。


sub exploit_workaround_4_1 {
        # This needs to come before your vcl_recv function
        # The following code is only valid for Varnish Cache and
        # Varnish Cache Plus versions 4.1.x and 5.0.0
        if (req.http.transfer-encoding ~ "(?i)chunked") {
                C{
                struct dummy_req {
                        unsigned magic;
                        int step;
                        int req_body_status;
                };
                ((struct dummy_req *)ctx->req)->req_body_status = 5;
                }C

                return (synth(503, "Bad request"));
        }
}

sub vcl_recv {
        # Call this early in your vcl_recv function
        call exploit_workaround_4_1;
}

5.1.x向け
インラインCは使用しません。


sub vcl_recv {
        if (req.http.transfer-encoding ~ "(?i)chunked") {
                return (fail);
        }
}

その他

実は今回の脆弱性について概要と公開予定時刻を事前に教えてもらっていました(攻撃には使用できないぐらいの荒い情報ですが)
お陰で公開後に即対処することが出来たのですが、VIVU – Very Important Varnish Usersで触れられているようにVMLを買うと教えてもらえるようなので(€1000/年)
企業でVarnishを使っているころは買ってみると良いのではないでしょうか?


8月 022017
 

4.1系の最新版の4.1.7がリリースされました。 [changelog] [公式のリリース文] [パッケージDL] [ソースDL]

今回の変更内容は主にstat周りのバグフィックスとちょっとした新機能です。

追加された機能

varnishncsaのLogFormatの%{X}xでrecord-prefixに対応しました(#2077)

もともとは%{VSL:tag[field]}xといった感じで指定できたのですが
Timestampのように同一タグでラベルで情報が違うものの場合、例えばProcessにかかった時間、Fetchにかかった時間を分けて出力することは不可能でした。(最初にマッチしたものだけなのでStartになる)
しかし今回record-prefixに対応したため、vsl-queryのような柔軟な指定が可能になりました。(levelには対応してないですが)


$ sudo varnishncsa -F "fetch:%{VSL:timestamp:fetch[2]}x process:%{VSL:timestamp:process[2]}x"
fetch:0.000377 process:0.000392

バグ修正

workspaceのリセット漏れでworkspaceが足りなくなって落ちるケースがあったのを修正(#2219)

varnishstatでのglobを修正(#2022,2118,2320)
あまり知られていないオプションだと思うのですが -fでstatのキー絞り込みをできるのですが
-1との組み合わせの時にうまく動かないバグがいくつかあったようです。

nuke_limitの効果がなくなっていたのを修正(#1764)

ヘルスチェックの応答でReason-Phraseがない場合に失敗していたのを修正(#2069)

(ちなみに今回記事が遅れたのは前の記事書いたあとあたりに書いたつもりになって忘れてました・・すいません)


7月 022017
 

最近何かと話題なCDNですが、そもそもCDNってなんだろう・・・どんなことに使えるんだろう?的なことを書いてみようと思います。
一応先に言っておくと、私はCDN業者に所属したことないのであくまでも利用者として見た時の話を書きます。
また、私の考えであり、様々なワークロードがあるなかでこれがすべてではありませんので、こんな考えもあるんだなぁぐらいに思ってもらえると助かります。

そもそもCDNってなんだろうか

そもそもCDNはContent Delivery Networkの略であってCache Delivery Networkの略ではありません。
要はコンテンツをクライアントに対して高速・効率的に配信するためのネットワークです。
良くCDNといえばその成り立ちからキャッシュというイメージはありますが、重要な要素の一つではあるもののCDNの全てではありません。
さらに言えばAkamaiのIntelligent PlatformやFastlyのEdge cloud platform、CloudFlareでは特に特有の名前はついていませんがOur platformと表現していますが、要はCDNはプラットフォームなのです。その上にキャッシュや後述する様々な機能などがアドオンされていると思ってもらえると良いかと思います。

CDNの作り方

CDNの正体とはなんでしょうか?
割とブラックボックスのように思えると思いますが、実はProxy(配信サーバ)とGSLB(広域負荷分散)を基本としたものです。
なので言ってしまうと、AWSのRoute53のGeo, Latencyベースのルーティングを使い、Proxyをいろんなロケーションに立ててしまえばあなたもCDN管理人に!
そして、作ったプラットフォーム上にサムネイルサーバを置いてしまえば動的サムネイル生成機能が付いたCDNになりますし、エンコーダーを・・・WAFを・・・みたいな感じにどんどん機能が増えていきます。
誤解を恐れずにすごく乱暴に言ってしまえば、これを大規模にやっているのがいわゆるCDN業者といえるでしょう。
また、既存CDN業者にない機能を使いたい、より柔軟性がほしい、そもそも大トラフィック扱っていて自社のほうがコストメリットがあるなどで自前でCDNを作る人達もいます(DIY CDN)。もちろん全部置き換える人もいればハイブリッドで使う人達もいます。
例えばSpotifyは去年から自社でCDNを立ち上げてハイブリッドで使っているようです。(Spotifyの日本インフラ)

CDNの機能について

さて、ここまででCDNはプラットフォームでそこにキャッシュなどの様々な機能をアドオンしたものとわかったと思います。
では、CDNにはどんな機能があるでしょうか?メジャーなものを幾つか紹介します。
当然ですが、ここで紹介する機能すべてがどのCDNにもあるもものではなく、あったりなかったりします。(なのでうまく選んでいきたいですね)

キャッシュ
やはりCDNといえばやはりキャッシュ!
貴方が例えばスマホゲームのインフラ担当の場合、当然アセットのダウンロードをどうしようか考えると思います。
リリース時に一気にダウンロードが走り、そのトラフィックは数百Gbpsを超えるケースもあります。
リリース時や更新時の一瞬のためだけにインフラを構築して・・・というのはやはり無駄があります。
ここでCDNの出番です。
アセットは一般的には静的なコンテンツなので、キャッシュが可能です。
まずユーザがリクエストしてきたら、最寄りのCDNのエッジサーバにリクエストがされます。
キャッシュがあればそこでレスポンスしますし、なければ自社のサーバ(オリジンサーバ)に対してリクエストが着ます。
このことによりオリジンサーバへのトラフィックを減らすことができ、なおかつユーザーからみるとネットワーク的に近いエッジサーバから取得することでより快適にダウンロードすることが出来ます。
そして一般的にCDNの帯域キャパシティは大きく、また大抵の場合は配信サイズ(1GBあたりいくら)での課金になります。
なので100Gbpsでようが1Gbpsだろうがピークをあまり気にせずユーザにアセットを配信することが出来ます。
(※CDN業者によってはQuotaが設定されていたりするので予め予定がわかってる場合は相談しておくと良いかと思います)
また他にも動的コンテンツのキャッシュもありますがあとで触れます。

Dynamic site acceleration(DSA)
キャッシュ以外での高速化を行う技術群です。
AkamaiFastlyではそれぞれDSAという名称で提供されており、他のCDNでも特に明記はしてないもののなんらかのDSA技術を使っているところがほとんどだと思います。
ざっくり触れていきます

・コンテンツのgzip圧縮
圧縮することで転送サイズを小さくして高速にします。

・エッジでのTLS終端
ユーザに近いエッジ(レイテンシが小さいことが期待できる)でTLS終端を行うことでハンドシェイクを高速に行えます。

・TCPの最適化
モバイルNW向けにリアルタイムにパラメータ調整したりするものや
エッジからオリジンまでを最適化したりなど
様々な手法でいろいろあります。

・コンテンツのプリフェッチ
ベースページを通した場合、その中に含まれる画像などのコンテンツをプリフェッチしてエッジを温めておくことでRTTを小さくします。

・ルーティングの最適化
インターネットは様々な経路でつながっています。
そのため、物理的な直線距離と実際の通信経路での距離は違い、またレイテンシも変わってきます。
身近な例で例えるのであれば、自宅のISPを乗り換えたら経路が変わって特定のサイトが早くなるということも十分考えられます。
その経路を最適化するのがこの技術です。代表例だとAkamaiのSureRouteが有名です。

他にもあると思います。
ちなみにsonotsさんがDSAについてふれて、実際にベンチをとっておられました(CloudFrontをかますとキャッシュなしのAPIコールでも速くなるようだ

Front End Optimization(FEO)
クライアントが実際にページを表示するにはざっくりいって

  1. ベースページのDL
  2. CSSやJSなどのリソースをDL
  3. ページレンダリング

といった流れがあると思います。
DSAの場合ではあくまでも配信を改善することが目的なので1,2でのネットワーク通信部分しか改善することはできません。
コンテンツに対してはほぼ手を加えず、手を加えるにしてもgzip圧縮程度で非可逆的な処置は行いません。(その分安全とは言える)
しかしサイトがリッチになるにつれ、ページレンダリングの処理が重くなっていき、場合によっては秒単位でかかるページも存在します。
じゃぁ、そこに対して何かできることはないか?というのがということで出てきたのがFEOです。
すごくざっくりいうとコンテンツの改変を含む最適化行うことで速度を向上しようと言うものです。
mod_pagespeedのようなことをCDNでやると言った感じです。
ベースページを解析して改変したり、画像をより圧縮効率の高い別フォーマットや端末に合わせた変換をしたり、CSSを調整したりなどいろいろ行います。
近年デバイス数も増えているので(モバイル)、すべての端末に対して最適化を手作業で行うのは難しいですが、CDN業者がその辺をメンテしてくれるのであれば楽ということです。

セキュリティ
最近は様々なサイバー攻撃が存在し、それをガードするためにCDNを入れることもあります。
DDoS対策やWAF、bot対策など様々なメニューがあります。

一つ注意としてですが、オリジンドメインがバレると直接そこにアタックされてどうしようもなくなったりするので
アクセス元を制限したり、類推しづらい名前にしとくのがよいかなとか・・(オリジンがわかりやすい名前でオリジンにアタックきたという事例を聞いたことあるので・・・)

最新技術への対応
最近であればHTTP/2など、様々な技術が出てきます。
しかし自前でやるには難しい環境の場合もあるかと思います。その場合でもCDNがproxyしてやってくれたりすると最新技術を利用でき、またそれで高速化のメリットを享受できます。

その他
他にもいろんな機能があります。
ビデオストリーミングや
機械学習でページ中の動的・静的部分を判別して静的部分をフラグメントキャッシュするもの
エッジ側にコードがおける(FastlyのVCL,CloudFrontのLambda@Edgeなど)などいろいろあります。

ユーザ情報を含むなどキャッシュできない動的なコンテンツをCDNに通すべきか通さないべきか

答えはどちらでもないだと思います
通す・通さないべきではなく、必要であれば通せばよいとおもいます。
先に触れたとおりCDNにはいろんな機能があります。
同一ドメインにある静的コンテンツをキャッシュしたいから通すみたいなケースが多いと思いますが
そもそも仮に全部キャッシュが出来なかったとしても、他の機能(セキュリティ対策など)が有用であれば使えばいいという単純な話だと思います。

しかし、動的コンテンツをCDNに通すのを気にしている人で個人情報が・・セキュリティが・・と気にする人がいます。
実際にCDN業者でもいろんな事件がありました
CloudFlareでのデータ流出バグ
CDNetworksのコンテンツ改ざん
このようなものは外部業者を使う以上一定のリスクは存在しうると考えていますので
結局ここは信用できるところを選ぶといった対策しかできません。
※CloudFlareやCDNetworksの事故を取り上げてますが、この2社が現在もヤバイとは思っていません。例えばCloudFlareはFBIに使用されてますし、CDNetworksはログインドメインは使用していないもののりそな銀行が使用しています。
しかし、これをもってそもそも使うのがNGというのは個人的には賛同できません。
多くのCDNではPCI DSSに対して配慮しているものがありますが(Akamai Fastly CloudFront CloudFlare CDNetworksなど)
そもそも動的コンテンツを流すことが想定されていなかったりすればこのような対応はされていないと思います。
参考程度ですがCitibank・USBank・ジャパンネット銀行・マネックス証券はログインフォームも含めてAkamaiを使用していました。


$ nslookup online.citi.com
Server:         127.0.0.1
Address:        127.0.0.1#53
Non-authoritative answer:
online.citi.com canonical name = online.citibank.com.edgekey.net.
online.citibank.com.edgekey.net canonical name = e11515.b.akamaiedge.net.
Name:   e11515.b.akamaiedge.net
Address: 184.30.150.120

$ nslookup onlinebanking.usbank.com
Server:         127.0.0.1
Address:        127.0.0.1#53

Non-authoritative answer:
onlinebanking.usbank.com        canonical name = onlinebanking.usbank.com.edgekey.net.
onlinebanking.usbank.com.edgekey.net    canonical name = e3644.ksd.akamaiedge.net.
Name:   e3644.ksd.akamaiedge.net
Address: 104.78.14.64

$ nslookup login.japannetbank.co.jp
Server:         127.0.0.1
Address:        127.0.0.1#53

Non-authoritative answer:
login.japannetbank.co.jp        canonical name = login.japannetbank.co.jp.edgekey.net.
login.japannetbank.co.jp.edgekey.net    canonical name = e2588.ksd.akamaiedge.net.
Name:   e2588.ksd.akamaiedge.net
Address: 184.26.113.168

$ nslookup mst.monex.co.jp
Server:         127.0.0.1
Address:        127.0.0.1#53

Non-authoritative answer:
mst.monex.co.jp canonical name = mst.monex.co.jp.akadns.net.
mst.monex.co.jp.akadns.net      canonical name = mst.monex.co.jp.edgekey.net.
mst.monex.co.jp.edgekey.net     canonical name = e4187.g.akamaiedge.net.
Name:   e4187.g.akamaiedge.net
Address: 69.192.239.108


このことからわかるように銀行や証券のようなすごくセンシティブな情報を扱うサイトでもCDNをつかうのはナンセンスではないということです。
結局のところは得られるメリットに対して、どれだけリスクやデメリットの程度、そして見合うコストなのかの話で、そもそもNGというわけではないです。(もちろん正しく使えればという話ですが)
例えば、2017年の今にクラウドはセキュリティが怖いからオンプレミスにするとか言うエンジニアがいたら割と?と思われると思いますが(たまにTwで見かけますが・・)
これはCDNでも同じ話が言えると思います。
CDN業者のセキュリティが怖いから自社でCDN作るぞ!みたいなこと言ったら流石にあなた達はAkamaiやAWSたち以上のことできるんですか・・・と思ってしまいます。
CDNであまりこういう意識がないのは単純な話としてCDNを使うエンジニアの母数が少なく、その辺の声が小さいんだろうなーとか考えています。

キャッシュをする、そしてしない

キャッシュを行うときに注意するべきなのはなんでしょうか?
一番重要なことは、一つのキャッシュするオブジェクトに対して複数の意味を持たせないということです。
例えばhttp://example.net/mypageというページがあって、仮にこれをキャッシュするとします。
名前からわかるようにこれはユーザ情報を含んでいます。
通常CDNなどでキャッシュする際はホスト名とパスが指定されますので
それに基づくとキーは「example.net + /mypage」になります。
しかしこれだとユーザ情報をキーに含んでいないため、Aさんが見た後にBさんが見た場合、Aさんのmypageが出る可能性があります。
この場合はmypageはAさんのページでもあるしBさんのページでもあるという複数の意味をもたせたから起きることです。
正しいキーは「example.net + /mypage + Aさん」といったようになります。
これはCDNでのキャッシュに限った話ではなく、例えばmemcachedやRedisといったKVSでも言えますし、他のキャッシュと呼ばれるものでも共通の考え方だと思います。
ちなみにこれはログイン情報に限った話ではなく、例えば、同じ画像URLでSP向けとPC向けでサイズやフォーマットが違う場合も、同様にキーにSPなのかPCなのかを含める必要があります。
また、クッキーの取扱にも注意が必要です。
2014年にキャッシュ設定の不備からクッキーが他ユーザに共有されてしまったというサイトがありました。
当事者じゃないので推測しかできませんが、おそらくキャッシュしたオブジェクトにSet-Cookieがついてしまってそれが使いまわされてしまったんだろうと考えています。

これは私がキャッシュを行うときの基本的なポリシーを図にしたものです。
必ずしもこれがすべてのワークロードで正しいものではありません。
例えば元々オリジンが静的コンテンツを置いているだけであればここまで複雑なルールは必要ないですが
動的・静的・かつユーザ情報含む全コンテンツを通すケースにおいては、参考にはなると思います

※この図では先程ふれたSP/PCで違う場合といったものはいれてませんが、そういうものがあればキーに含めてください。
※ユーザ情報はクッキーに含まれていることを想定しています。なにか他のものに入ってる場合は適宜消す対象を含めてください(POSTとか注意したいですね)
※他にキーに設定したりするものとしてはCORSのOriginヘッダなどがあります。これはVaryで分けてもいいですが、オブジェクト自体が変わらないのが通常なので、CDNに条件付きでレスポンスヘッダのみの書き換えができるのであればでそちらで行うのが良いでしょう。(AkamaiやFastlyなどはできる)

まず、キャッシュをしない場合は必ずCDN側で抑制すべきと考えています。(もちろんCache-Controlを適切に設定するのも大事です)
CDN業者によって挙動が変わるため、確実なのはCDNの機能で「確実にキャッシュさせない」ことです。
なぜかというと単純な話として出口で対策をかけるのが確実だからです。
また、基本的に静的コンテンツしか載せないのであればブラックリスト方式(基本キャッシュで一部非キャッシュ)でもいいとおもうのですが、
サイトすべてをCDNに流していて、そこにユーザ情報を含むもの(キャッシュ・非キャッシュにかぎらず)があるのであればホワイトリスト方式(基本非キャッシュで一部キャッシュ)で運用すると安心できると思います。

また、私はあまりオリジンのアプリを信用していません。
例えばこのパスはキャッシュが可能でユーザ情報によって変化もしないのが確実だからCookieをオリジンに送信するとします。
しかし実際はアプリはCookieからユーザ情報を読み取って、ユーザ毎にコンテンツを生成していたらどうなるでしょうか? 流出です。
なので、大事なのはキャッシュをする場合はオリジンに対して不要なキー(Cookie)は送信しないということです。
Cookieを消しておけば、仮にアプリがユーザ情報を元になんらかのコンテンツを生成しようとしても情報がないので、ログインページに飛ばされるなどクリティカルではない障害ですみます。

また、同様にSet-Cookieも消すべきです。
これはセッションが共有されるのを防ぐというのが主目的です。
ではなぜキャッシュキーにユーザ情報が含んでる状態でも消すのかというと、cookieのexpiresの問題や何らかのワンタイムトークン的なものが含まれていたりとか合ってよくわからない動作になるのを防ぐためです。

TTLの取扱に気をつけよう
静的コンテンツの場合は、頻繁に同一URLでのコンテンツの差し替えがないのであればTTLはあんまり気にする必要はないのですが(その場合もpurgeすればいい話ですが)
動的コンテンツの場合はモノによっては注意が必要です。
例えば5分毎に更新されるコンテンツが1日もキャッシュされたら困るからです。
静的・動的と言ってますが要はそのURLで別のコンテンツが提供される可能性があるのであれば(差し替えであれ、定期更新であれ)TTLには注意しようと言う話です。
ここはそのコンテンツの特性に応じていろんな考え方があるのでこれで確実といったのは特にないので・・・(例えば必ず毎時0分に切り替わる必要があるとか、多少ぶれてもいいものとかいろいろある)

また、テストも重要です。
CDNのテストってどうやるんだ・・・という話もありますが、大抵の場合はそのオブジェクトがキャッシュ対象かなどをレスポンスヘッダで教えてくれたりします。(Akamai)
なのでその辺を元にテストを行えば問題ないでしょう。
テストパタンを考えるのはそんなに難しくはないと思います。
ユーザ情報を含むページも通してるのであれば、複数ユーザ+非ログインユーザで条件が変わるパス毎にテストをすればよいだけです。
そして意図した動作(Cookie/Set-Cookie消してるとかキャッシュしてるしてないなど)になっているかの確認をしましょう。
先程の図では触れていませんがキャッシュのテストをするときは、割とTTLも重要なので動作確認をしましょう。
TTLの動作(特に切れた場合)もCDN事業者によっていろいろあると思いますし設定可能です。
すべてのパタンではないですが、例をあげると
TTLが切れた場合でオリジンがエラーを吐いている場合はとりあえず古いオブジェクトを使う
という動きをするケースもあります。(Optionでなど)

ここまで言っておいてなんですが、すべてのCDN業者でこれほど自由度のある設定ができるとも限りません。
特にユーザ情報を含むコンテンツをキャッシュする際はかなりキャッシュキーに注意する必要がありますので、CDN業者でここまで設定できないのであればユーザ情報を含むコンテンツのキャッシュは辞めるべきです(そもそもやって意味があるのか的な話は後で触れます)、単純にキャッシュせずに通すだけであれば問題ないです。
他にも、CDN側で抑制出来ないパタンがあるのであれば、オリジン側のProxyでリクエスト・レスポンスヘッダのクレンジングするのも良いと思います。(同様のルールでクッキーの制御を行う)
オリジン側のProxyでクッキーを消していれば仮にCDNでキャッシュされても、やっぱり非ログインページになるのでそこまで痛くないでしょう。(もちろん2重管理にはなるのと完全にはカバーできないのでその辺は注意)
その場合はよりCDNの仕様を注意深く読み・問い合わせ、入念にテストを行うべきです。

あと、この辺の複雑な制御はFastlyがCDNの中でもトップレベルと考えています。
理由としてはVCLというDSLでキャッシュするしないの制御を始め、クッキーからユーザ情報を抽出してそれをキャッシュキーに設定するなど細かい制御が可能なためです。
例えば/admin/というページはキャッシュをしてはならないというルールをVCLで書くと


sub vcl_recv{
  if(req.url ~ "^/admin/"){
    return(pass);
  }
}

と言った感じになります。

ユーザ情報を含む動的コンテンツを効率的にキャッシュするには

ユーザ毎に生成されている動的コンテンツをキャッシュするのは難しいですし、普通はやりません(ある意味最終手段とも言える)
難しい理由としては

  • キャッシュ混じりなどが起きないようなキーの管理
  • ページを構成する要素のTTLが同期していない
  • そもそも再利用されるのかという話

こんな感じです。
キャッシュを行うのは何度も使用されることを期待して行うものですが、当然ながらユーザ毎にキャッシュしたものはユーザしか使用しません。
TTLが同期していないという話は例えば、ページ中に5分おきに更新される直近売れたものリストとユーザの行動によって変わるレコメンドがあるなど(これならXHRでやればいいという話もありますがたとえなので)
一体どれだけのTTLでキャッシュすればいいかがわからないケースです。
じゃぁどうすればいいかというと、少し発想を変えてみて部品単位で考えてみることです。
そうすると案外再利用されたりするものがあったりします。
もちろんそのための規格もあって、Edge Side Includes(ESI)というものです。
ざっくりいうとフラグメントキャッシュ持っておいてCDN側で結合する感じです。
これにより

  • 部品単位で適切なTTL、更には適切なキーでキャッシュできる(そのページがユーザ毎だったとしても部品がユーザ毎とは限らない)
  • 再利用しやすい

というふうになりますので
うまくすれば通常のサイトの場合は半分以上は実際キャッシュできると思います。
もちろんキーの管理とか難しいですし、バラバラに分解した上でユーザ情報を含まないところだけでキャッシュするだけでもかなり効果があります。
また、ゲームのようなあまり向かないものもありますのできちんとワークロードにあってるかは検討する必要がありますが良いかなと思います。
かなり昔に書いた資料ですが参考になるかもなのでよかったらどうぞ(VarnishではじめるESI)

まとめ

ここまで割とCDN使っていこうぜてきなノリで書いてきましたが、それぞれメリットやデメリット等いろいろあります。そしてもちろんコストもかかりますので、見合う見合わないを適切に判断した上で楽になれば使えば良いと思いますし、そうでないなら使わなければ良いと思います。
しかし動的コンテンツをそもそも流すのはナンセンスではないということは理解していただけたんじゃないかなーとか思います。
CDNも結局一つのレイヤーとして考えてもらって、もし貴方がCDNが解決できるかもしれない課題で悩んでるのであればこの記事が助けになれば良いかと思います。


 Posted by at 2:23 AM
5月 052017
 

4.1系の最新版の4.1.6がリリースされました。 [changelog] [公式のリリース文] [パッケージDL] [ソースDL]

今回の変更内容は基本的にバグフィックスと5.1.1/5.1.2で追加された一部機能の取り込みになります。

追加された機能

すべて5.1.1/5.1.2で追加された機能です。
vmod_stdにgetenv(STRING)の追加
varnishtestにsetenvの追加
vsl-queryでvxid指定が可能に

バグ修正

Ageレスポンスヘッダの数値が実経過時間より大きな数値になるケースがあったのを修正しました(#2216)
Ageレスポンスヘッダの小数点の丸めをsprintfの%.0fで行っていたため、実際の経過時間より大きな数値になることがあったんですが
切り捨て(floor)を行うようにした感じです。

std.querysortでワークスペースが足りない際にpanicしてたのを修正しました(#2233)

稀にVSLを利用するクライアントが初期化に失敗するケースがあるのを修正しました(#2241)
あくまでに初期化時(VSM_Init)のみなので稼働中のクライアントの場合には問題ありません。
自分もVSLを利用するクライアントを作ったり使ってますが遭遇したことがないのでかなり稀だと思います。

クライアントワークスペースの空き容量が少ない場合にチャンク転送を行った場合にpanicしていたのを修正しました(#2275)

動的バックエンドを利用している場合で稀に子プロセスが再起動するのを修正しました(#2295)


4月 072017
 

5.1.2がリリースされました。
5.1.1の記事で4月中旬まで様子を見たほうがよいと言っていたのはこの件です。
(まだ確定じゃなかったので微妙にぼかした表現でしたが・・)
今回のリリースは1件の機能強化と主にHTTP/2関連のバグフィックスです。
[changelog] [リリース文] [パッケージDL] [ソースDL]
 

機能強化

std.collectでセパレータを指定できるようになりました
std.collectは同一名のヘッダが複数行送られてきた場合に1行にまとめる関数ですが今までは[, ]でまとめられていました。
通常の場合は特に問題ないのですが、Cookieの場合は[; ]でまとめる必要があり、追加で置換が必要など面倒でした。
デフォルト値には[, ]が指定されているので、既に使用している場合は特に追加指定は不要ですが
Cookieで使用する場合は以下のように指定するとよいです。


std.collect(req.http.cookie, "; ");

 

バグ修正(h2以外)

#2295 動的バックエンドを使用しているときにヘルスチェックが無限ループに陥ることが合ったのを修正
#2207 #2278 PROXYプロトコルを使用したバックエンドのヘルスチェックで競合状態がおきるのを修正
 

バグ修正(h2)

#2291 #2300 Cookieヘッダを1行にまとめるように修正
#2247 ボディを含むリクエストにContent-Lengthが存在した場合でもTransfer-Encoding: chunkedを追加していたのを修正
上記2つの修正により、前回紹介したVCLトリックは不要になりました。

#2238 ReqAcctの値がないためvarnishncsa等で入出力したバイト0になるのが修正されました
わかりやすいところだとvarnishncsaの%bなどが常に0になっていたのですが、この部分が実装されたためサイズがでるようになりました。

多数のassertを修正

Varnishでのhttp/2について

5.1.1では検証環境で使用できると書きましたが、今回は十分な検証を行った上であれば本番環境に入れることもできなくはないと考えています。
もちろんまだexperimentalで残バグもおそらく新規バグもあると思いますが
主にSimonや自分が相当数踏み抜き、修正されているので
以下の幾つかの注意点を守れば比較的安定的に動作すると思います。

#2268 PRIV_TASK/TOPを利用しているvmodを避ける
PRIV_TASK/TOPについてはこちらの記事を御覧ください。
このPRIVの確保・解放を行う際に競合状態が起きているようで、稀にassertが起こります。
現時点で回避するには使用しているvmodの関数を避けるしかありません(例:vmod_cookie)
使用しているかどうかの判定はvccファイルを見てPRIV_TASKかPRIV_TOPがあるかで判定できます。

スレッドのメモリ使用サイズがh/1の時と比べて大幅に増える可能性があるのでチューニングが必要
h2はh1と比べるとメモリをより多く使用するため一瞬なんか漏れてると思うぐらい増えます。
元々メモリに余裕がある環境であれば特に問題はないのですが、VPSなどあまりメモリがない環境の場合は問題といえます。
解決方法としては単純にスレッド数に余分があれば減らす感じです。
もし、減らしすぎてスレッドが足りなくなったとしても、そのリクエストはキューイングされて、すぐにスレッドを追加作成して処理されます。
関連するパラメータとカウンタはこれです。

パラメータ
thread_pools スレッドプール数
thread_pool_min スレッドプール毎の最低スレッド数
thread_pool_max スレッドプール毎の最大スレッド数
thread_queue_limit スレッド枯渇時のキュー長
カウンタ
MAIN.threads_limited ドロップされたセッション数
MAIN.sess_dropped ドロップされたセッション数
MAIN.sess_queued キューイングされたセッション数
MAIN.threads 現在のスレッド数

スレッド数はthread_pools * thread_pool_min以上thread_pools * thread_pool_max以下となります。
チューニングをする際はMAIN.sess_droppedが起きるのは論外で、MAIN.sess_queuedもそんなに起きないのが望ましいです。
このあたりのチューニングは今回の本筋ではないので詳しくは解説しませんが、要は過剰であればthread_pool_minを減らしてみて
MAIN.threadsが止まったあたりでdropped/queuedも起きないように調整するとよいのではとか思います。
本当にメモリが辛い場合は多少パフォーマンスは落ちますがqueuedを覚悟してthread_pool_minをガッツリ下げてthread_queue_limitを上げるのも手だと思います。(その前にスペックとか他のパラメータを再検討したほうが良いと思いますが)
結局のところ、ここでいいたいのはメモリの使用傾向が変わるとおもうのできちんとチューニングしようねって話です。
追記:上記で長めに運用しましたが、多少ましになる程度でした。(とはいえスレッド数の調整はしたほうがよいかなと思います。

とはいえ、HTTP/2を使う際はpkg-varnishcacheを使ってなるだけ最新を追っておくと良いかと思います。

hitch1.4.4を使う場合の注意事項
hitchは設定のreloadをサポートしているのですが(HUPを投げる)
ocsp staplingが有効だとreload時にゾンビプロセスが残る場合があります(#167)
以下のように設定して無効にすることで回避も可能です。


ocsp-dir = ""

あくまでoffにすることを推奨しているのではなく、ここは各自考えてといった感じでお願いします。

最後に
ぜひVarnishのhttp/2を試してみてほしいなと思います。
また、Varnishの開発を助けるためにVMLを購入するのをぜひ検討してみてください


4月 022017
 

5.0.0の後継の5.1.1がリリースされました。 [changelog] [リリース文] [changes] [Upgrading]
また、今回からパッケージがpackagecloudで提供されるようになりました。(packagecloud)
5.1.0はどうなったの?という話ですが、ちょっと問題となるバグがあったため翌日にすぐ5.1.1を出した感じです。
今回のリリースでは4.1.4/4.1.5で変更された内容と細かい機能修正(VCL変更含む)があります。(それについては割愛してます)
VCLの変更はあるものの修正しないと動かないような変更はないのでよほどのことがない限りそのまま動作するはずです。
なお、VMODについては動かなくなるものもあるので事前に使用しているものが動くかを確認したほうが良いでしょう。
また、累積バグも修正されているのですが、よほど修正されているバグで困ってない限りは本番への適用は一旦4月中旬までは様子見たほうが良いかと思います。(何もなかったらすいません)

機能強化

varnishdのパラメータ追加(-I [cli file])
workerが起動する前に指定されたcli file読み込まれます。
このファイルはvarnishのcliコマンド(vcl.loadやvcl.useなど)が列挙されています。
これがなんの役に立つのかというと、5.0.0で追加されたvclのラベル機能を使っている人には非常に便利な機能です。
以前紹介したとおりlabel機能を使うには先にvcl.loadを利用してラベルを定義する必要がありました。
しかしvarnishdでvclを読み込む-fオプションではラベルを指定できなかったため、有効に使うには別にスクリプトを作るなど若干めんどくさかった面も否定できませんでした。
しかしこのオプションを使うことでworkerが起動する前にラベル付きでvclを読み込むことができるので非常に楽になります。


vcl.load panic /etc/varnish_panic.vcl
vcl.load siteA0 /etc/varnish_siteA.vcl
vcl.load siteB0 /etc/varnish_siteB.vcl
vcl.load siteC0 /etc/varnish_siteC.vcl
vcl.label siteA siteA0
vcl.label siteB siteB0
vcl.label siteC siteC0
vcl.load main /etc/varnish_main.vcl
vcl.use main

このようなcliファイルを作っておいて


sub vcl_recv {
    if (req.http.host ~ "asite.example.com$") {
        return(vcl(siteA));
    }
    if (req.http.host ~ "bsite.example.com$") {
        return(vcl(siteB));
    }
    if (req.http.host ~ "csite.example.com$") {
        return(vcl(siteC));
    }
    // Main site processing ...
}

このようなVCLを使うことでラベル機能を使うことが出来ます。

なお、コマンドの先頭に「-」をつけることでそのコマンドが失敗しても処理を続行することができます


起動に失敗する
vcl.load nf /etc/varnish/notfound.vcl
 
起動に失敗しない
- vcl.load nf /etc/varnish/notfound.vcl

varnishdのパラメータ追加(-x (parameter|vsl|cli|builtin))
これは何かしらの動作がというわけではなくて以下の情報が表示されるコマンドです

parameter
起動パラメータ(-p)の情報(デフォ値など)

vsl
vslのタグや出力フォーマットの説明

cli
varnishのcli(vcl.listなど)の説明を出力します

builtin
builtin.vclを出力します

HTTP/2がより使えるようになりました
5.0.0のHTTP/2は正直検証環境でも使うには辛いものだったのですが(POST投げると落ちるなど)
5.1.1では一部気をつけるポイントはあるものの検証環境で使えるぐらいには動きます。
また、HTTP/2関連は5.1.1リリース後も積極的にバグ修正が行われており、もし使いたい場合はこの記事で紹介したpkg-varnish-cacheを使って最新のコードで試すことをおすすめします。
なお、気をつけるポイントですが以下のとおりです。

  • 転送サイズ(varnishncsa %I %Oなど)などの統計がすべて0 #2238
  • bodyがあるリクエストでcontent-lengthがある場合でもTransfer-Encoding: chunkedを付与する #2247
  • クッキーの畳み込みを行わないのでVCLでクッキーの操作を行う場合に問題がある #2291 #2300

です。
統計については現時点ではどうしようもないのですが、ほかについては以下のVCLで一旦回避可能です。
もちろん修正された場合は不要となります。


sub vcl_recv{
  if(req.http.cookie){
    // temporary...
    // https://github.com/varnishcache/varnish-cache/issues/2291
    // https://github.com/varnishcache/varnish-cache/issues/2300
    std.collect(req.http.cookie);
    set req.http.cookie = regsuball(req.http.cookie,", ","; ");
  }
  if(req.proto ~ "HTTP/2"){
    if(req.http.content-length){
      // temporary...
      // https://github.com/varnishcache/varnish-cache/issues/2247
      unset req.http.content-length;
    }
  }
}

なお上記で行っているクッキーの畳み込みですが、もしESIを利用していない場合はHTTP/2に限定しても問題ありません。
他の注意点としてはPRIV_TASK/PRIV_TOPを利用しているvmodについては現時点では避けておいたほうが無難です。(HTTP/1では問題ありません)
またHTTP/2関連のパラメータとして以下が追加されています

  • h2_rx_window_increment
  • h2_rx_window_low_water

デフォルトは10MBで十分大きいので特に変更する必要ははなさそうですが、
大きなPOST/PUTを受ける場合は大きくしても良いかと思います。

即処理を終了するためのreturn(fail)が追加されました
これはどのvclアクションからも呼び出すことができます。
なおfailの後はvcl_synthがコールされますが、デフォルトでresp.statusには503がresp.reasonにはVCL failedが入っていますので、特に操作をしない場合はクライアントからは503が見えます。
なおvcl_synth内でfailを呼び出した場合は接続をアボート(即切り)します。

vcl_backend_responseでreturn(pass(DURATION))が追加されました
要はhit-for-passです。
TTLを同時に指定することができるので以前より指定しやすくなったので便利かと思います。
この辺(HFP等)わかりづらいとおもうので動きの解説記事をそのうちかきます・・

vsl queryでvxidの指定ができるようになりました
vxid(x-varnishレスポンスヘッダ)はリクエスト毎に発行されるので当然ながら事前にわかりません。
ヘルスチェックなどはvxid=0で実行されるのでそれの絞込ぐらいにしか使えないのかというとそうではなく
varnishncsaなどのlog系オプションの-dと組み合わせることで強力に使用できます。
varnishのログはメモリ上(VSM)に保存されるのですが、これはリングバッファのため古いログも多少残っています。
-dオプションは残っているログの先頭から出力するオプションのため、リクエストを行った後に取得したvxidを指定することでvarnishの挙動の確認を行うことが容易にできます。

fallback directorにstickeyオプションを追加しました


new fb = directors.fallback(sticky = true);

fallbackのdefaultの挙動では優先度の高いバックエンドがsickになった場合は当然fallbackします。
今回のstickeyオプションを指定するとこの優先度の高いバックエンドがhealthyになったときの挙動が変わります。
指定がない場合は優先度の高いバックエンドが選択されますが、指定がある場合はそのまま今繋いでるバックエンドを使用し続けます。
この動作は順繰り行われ、一番優先度の低いバックエンドがsickになった場合は再度先頭から評価されます。

起動時にvmodのバックアップを行うようにしました
これはVarnishの起動しているときにvmodの差し替えが行われて、その後の操作(reloadなど)でおかしくなることがあったので、使うvmodについてはバックアップを取るようになりました。

IPアドレス同士の比較ができるようになりました
vmod_stdにgetenvとlate_100_continueが追加されました

仕様変更

vcl_dirはvcl_pathに変更されます(起動パラメータ)
vmod_dirはvmod_pathに変更されます(起動パラメータ)
req.ttlは廃止予定です
beresp.storage_hintは廃止予定です
完全に同じものではないのですがberesp.storageで代替可能です。
レスポンスコードを1000以上でも指定できるようにしました
これはあくまでもVCL中で使うためのもので、実際にレスポンスされるときは1000以上の桁は削られます。
要はreturn(synth(1000))とかで指定しておいて、vcl_synthで処理するのに使う感じです。
DURATION型をBOOL値として使用することができるようになりました
0以下はfalseでそれ以外はtrueです
TIME型にDURATIONを加算・減算できるようになりました
REAL型にINTを加算・減算できようになりました

パラメータの追加

ban_cutoffが追加されました(起動パラメータ)
req.storageが追加されました
これはrequst bodyを保存するのに使用されます。
未指定の場合はTransientが使用されます。

バグ修正

基本的には4.1.4/4.1.5で修正されたものが大きいものだったので割愛します。