xcir

\いわなちゃん/

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月 042015
 

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

Varnishは4.0まではfile-storageを使用する場合は特にpre-allocateをしないため、フラグメントによるパフォーマンス低下を防ぐためにdd(1)を使って先に確保してしまうのを推奨していました。
4.1でも事前確保するのを推奨しているのですが、実はbetaまではfallocateを利用してpre-allocateするようになっていました。
正式版ではこの機能はext4限定になってしまい他のファイルシステムではdisableになりました。
何故かxfsだと既に領域を確保しているにもかかわらずfallocateを使おうとするとENOSPCがでるケースがあったためです。
このバグの報告をしたのは自分なのですが、その際にfallocateについて調べてたら割と興味深い動きだったので今回書きます。

fallocateとは
すごく簡単にいうとめっちゃ高速にディスク領域の割当ができます。
どれぐらい速いかというと


# time fallocate -l 10GB varnish_storage.bin

real    0m0.001s
user    0m0.000s
sys     0m0.001s


と気にならないレベルです。
割と便利だと思います。

fallocateの不思議な動き
xfsとext4でfallocateの動きを確認した際に以下のような動きをしました。

xfs-fallocate


root@varnish-trunk:/mnt/xfs# df -T .
Filesystem     Type 1K-blocks  Used Available Use% Mounted on
/dev/xvdh      xfs   15718400 32928  15685472   1% /mnt/xfs
root@varnish-trunk:/mnt/xfs# fallocate -l 10GB varnish_storage.bin
root@varnish-trunk:/mnt/xfs# df -T .
Filesystem     Type 1K-blocks    Used Available Use% Mounted on
/dev/xvdh      xfs   15718400 9798556   5919844  63% /mnt/xfs
root@varnish-trunk:/mnt/xfs# fallocate -l 10GB varnish_storage.bin
fallocate: varnish_storage.bin: fallocate が失敗: デバイスに空き領域がありません
root@varnish-trunk:/mnt/xfs# ls -ltah varnish_storage.bin
-rw-r--r-- 1 root root 9.4G 12月  3 23:55 varnish_storage.bin

ext4-fallocate


root@varnish-trunk:/mnt/ext4# df -T .
Filesystem     Type 1K-blocks  Used Available Use% Mounted on
/dev/xvdi      ext4  15350768 38384  14509568   1% /mnt/ext4
root@varnish-trunk:/mnt/ext4# fallocate -l 10GB varnish_storage.bin
root@varnish-trunk:/mnt/ext4# df -T .
Filesystem     Type 1K-blocks    Used Available Use% Mounted on
/dev/xvdi      ext4  15350768 9804016   4743936  68% /mnt/ext4
root@varnish-trunk:/mnt/ext4# fallocate -l 10GB varnish_storage.bin
root@varnish-trunk:/mnt/ext4# ls -ltah varnish_storage.bin
-rw-r--r-- 1 root root 9.4G 12月  3 23:56 varnish_storage.bin

なぜかxfsは同じ容量を確保しようとしてるのに失敗する。

xfs-stat


root@varnish-trunk:/mnt/xfs# fallocate -l 1GB varnish_storage.bin
root@varnish-trunk:/mnt/xfs# stat varnish_storage.bin
  File: `varnish_storage.bin'
  Size: 1000000000      Blocks: 1953128    IO Block: 4096   通常ファイル
Device: ca70h/51824d    Inode: 131         Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2015-12-03 23:58:42.851567000 +0900
Modify: 2015-12-03 23:58:47.475567000 +0900
Change: 2015-12-03 23:58:47.475567000 +0900
 Birth: -
root@varnish-trunk:/mnt/xfs# fallocate -l 1GB varnish_storage.bin
root@varnish-trunk:/mnt/xfs# stat varnish_storage.bin
  File: `varnish_storage.bin'
  Size: 1000000000      Blocks: 1953128    IO Block: 4096   通常ファイル
Device: ca70h/51824d    Inode: 131         Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2015-12-03 23:58:42.851567000 +0900
Modify: 2015-12-03 23:59:15.123567000 +0900★変わった
Change: 2015-12-03 23:59:15.123567000 +0900★変わった
 Birth: -

ext4-stat


root@varnish-trunk:/mnt/ext4# fallocate -l 1GB varnish_storage.bin
root@varnish-trunk:/mnt/ext4# stat varnish_storage.bin
  File: `varnish_storage.bin'
  Size: 1000000000      Blocks: 1953136    IO Block: 4096   通常ファイル
Device: ca80h/51840d    Inode: 12          Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2015-12-03 23:59:35.871567000 +0900
Modify: 2015-12-03 23:59:35.871567000 +0900
Change: 2015-12-03 23:59:35.871567000 +0900
 Birth: -
root@varnish-trunk:/mnt/ext4# fallocate -l 1GB varnish_storage.bin
root@varnish-trunk:/mnt/ext4# stat varnish_storage.bin
  File: `varnish_storage.bin'
  Size: 1000000000      Blocks: 1953136    IO Block: 4096   通常ファイル
Device: ca80h/51840d    Inode: 12          Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2015-12-03 23:59:35.871567000 +0900
Modify: 2015-12-03 23:59:35.871567000 +0900★変わってない
Change: 2015-12-03 23:59:35.871567000 +0900
 Birth: -

xfsは同じサイズでfallocateを叩くとタイムスタンプが変わる。

xfs-filefrag


root@varnish-trunk:/mnt/xfs# fallocate -l 1GB varnish_storage.bin
root@varnish-trunk:/mnt/xfs# filefrag -v varnish_storage.bin |md5sum
8a5e67e82aa00af5af01c6d825bd8142  -
root@varnish-trunk:/mnt/xfs# fallocate -l 1GB varnish_storage.bin
root@varnish-trunk:/mnt/xfs# filefrag -v varnish_storage.bin |md5sum
8a5e67e82aa00af5af01c6d825bd8142  -

ext4-filefrag


root@varnish-trunk:/mnt/ext4# filefrag -v varnish_storage.bin |md5sum
9785097e3ce0e6eb3dd703fbd54e5d69  -
root@varnish-trunk:/mnt/ext4# fallocate -l 1GB varnish_storage.bin
root@varnish-trunk:/mnt/ext4# filefrag -v varnish_storage.bin |md5sum
9785097e3ce0e6eb3dd703fbd54e5d69  -

両方共、physical_offsetに変化なし
ちなみにext4の場合は割とフラグメントしますがxfsの場合はあまりしませんでした。
(10GB確保時にext4=12~50extents xfs=3~5extents)

これらを見てみると恐らくxfsは毎回空き容量と確保したい容量を付きあわせていて(既にallocateされてる容量は無視?)
ext4は最初にコマンド実施が必要かどうかをチェックしてるように見えます。
なんでこうなるのかxfsのコードを少し追ったのですが流石にキツかったので途中で諦めました。

まとめとか
fallocateは速くて便利なんでdd使ってpre-allocateしてる人はfallocate使うと幸せになれると思います(とは言えext3とかはサポートしてませんが)
そしてvarnish4.1をext4で動かす場合は内部でfallocateするので、今まで手動でやっていたpre-allocateは不要です。
もし、自分でfallocateを使ったコードを書く場合は実行前にfstat等で実際に使用している容量を調べるなどして使うと良いと思います。


 Posted by at 12:43 AM
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)


11月 162015
 

だいぶプライベートがゴタゴタしていて(主に引越で)今更の話で申し訳ありませんが次世代 Web カンファレンス(以下nextwebconf)のserver_perfで登壇する機会を頂いたのでその事を書いておこうと思います。

まず最初に、登壇者を見た時凄いメンツ過ぎてビビったというのと、それを実現するJxckさんのすごいなと思いました。
スライドなしでトークのみで議論するというイベントなのでぶっつけ本番かというとそうではなく、当然ながら僕にとってserver_perfってなんだろうというのをイベント当日、そして終った後も考え続けました。そういう意味では割と準備にエネルギーがかかったものだと思います。
おかげであやふやだった自分の中でのイメージが、一定のまとまりを得たというのも非常に成果でした。
このエントリはその際に作ったメモの一部を綺麗にしたものです(全部がパフォーマンス関連というわけではないです)
結論というわけではなく、あくまで僕の考えということをご留意ください。

そうえばサーバサイドパフォーマンスってなんだっけ

サーバサイドはともかくとしてパフォーマンスってすごく広い言葉だと思います。
単純にレスポンスが高速になるというのもパフォーマンスでしょうし
唐突の高負荷にも、サービスを落とさずオートスケールしていくのもパフォーマンスだと思いますし、いろいろあります。
この記事では主にレスポンスについて、そしてネイティブというよりブラウザよりです。

そもそもクライアントとサーバの境界線は何処だろう

いきなり昔話ですがWindows95~98がでたあたりのWebは非常に簡単でした。
ブラウザで動くコードは、せいぜい動いてマウスを追跡するスクリプトぐらいで、単純にDC等においてあるマシンから静的なファイルや、はたまたCGIで掲示板などを動作させてtableタグで段組が終わったHTMLを生成してレスポンスしていたと思います。
地図サイトも、今のようにシームレスに動くようなものではなく、クリッカブルマップで座標を変えていくものでした。
当時のブラウザとDCにあるマシンは、クライアントとサーバの関係でした。
しかしJavascriptがどんどん強力になり、少し前にAjax、そして最近ではHTML5/ServiceWorkerが出てきて、今までクライアントであったブラウザの中にサーバのようなものが出てきてからは、スパっとクライアントとサーバを分けることが難しくなってきています。
昔であれば、サーバサイドでかかった時間やファイルサイズ等のサーバサイドだけを見ていればそこまで大きなズレはありませんでしたが
最近ではサーバサイドをいくら速くしても、ブラウザで動くスクリプトがアレであればレンダリングが秒単位で遅れることなんてのも珍しくありません。
パフォーマンスを考える上で相対的にサーバサイドの重要性が下がり、クライアント側の重要度が上がってきているといえるでしょう(もちろんサーバサイドを御座なりにしていいというわけではないです)
言うならがスパっと境界線が引ける状態ではなくserver/clientのどっちに軸足を起きつつ両方見れるかというのが重要になってきているといえます。
また、誰がコントロールするかというのも意識する必要があると考えています。
サーバであればコントロールの主体は僕らなのでスペックやデータのコントロールもしやすいのですが、クライアントのコントロールの主体は必ずしも僕らではなくスペックもバラバラでデータのコントロールもしづらいです。

個人的に考えている次世代のサーバサイドパフォーマンス

クライアントの処理性能はどんどん向上し、PC・モバイルともにNW環境もよくなり、以前に比べて非常に環境がよくなりました。
しかしWebは高速になったでしょうか?
僕は必ずしもそうではないと考えています。
コンテンツがどんどんリッチにヘビーになっているということ、良くなったNW環境をまだ効率的に使えていないと考えています(HTTP/2が普及するといいなぁ)
そのような中で如何にパフォーマンスを考えていくかというと一つのヒントが昨年のAkamai Edge14カンファレンスのビデオ(Meeting the Grand Challenges of the Internet)の中にあると思います。
この中でVOD関連のキーワードとしてMOVING INTO HOMESというのが出てきています。
これは主にVODにおいて爆発するトラフィックについての話なのですが、僕はそれ以外でも言える話だと考えています。
僕にとっての次世代のサーバサイドパフォーマンスを考える上で

  • データは何処にあるのか
  • データはその場所にいつからあるのか
  • そのデータは個別のクライアントにとって最適であるか

が重要だと考えています。
すごく噛み砕くと

  • 如何にクライアントの近い場所にデータを置くか
  • プリフェッチ・プッシュ・キャッシュ
  • クライアントに応じた最適化

です。
如何にネットの環境が良くなろうとも、物理的に近いほうが良いのに決まっています(そして効率のよいプロトコル)
最近はありがたいことにクラウドがあるので、物理的にクライアントの近い場所にインスタンスを置き、そこからレスポンスをするということが大規模サービス事業者でなくても可能になりました。(選択もRoute53で楽になりました)
ただ、全世界で一つのデータソースを元に何かするということはいろいろ考えることがあり難しいです。
そこで重要なのが、クライアントに応じた最適化そしてキャッシュだと考えています。

まずクライアントに応じた最適化ですが、いわゆるFEOをはじめとするManagedな最適化サービスです。(AkamaiのIonやinstartlogicなど)
クライアント側のコードの最適化は複雑化してクライアントの種類(特に端末)も増えてもはや人力で全てのページで100%を目指すというのは辛くなっていると考えています。
そのためFEOのような透過的に最適化を行うロジックが今後も発展すると考えています。
なによりこれが素晴らしいのが最悪キャッシュが出来ない場合でもパフォーマンスの向上を期待できることです。
もちろん大抵の場合はキャッシュと併用できるはずですが・・・

次キャッシュの場合です。
キャッシュで設定するTTLですが、すごく乱暴な話をしてしまえばその期間は物理的に何処にデータを置いてもいいのです。(もちろんクライアントの近くに)
では単純に、CDNやクラウドのいろんなリージョンにキャッシュサーバを立ててしまえば解決でしょうか?
僕はそうは考えていません。
ここでもう一つ重要なこととしてロジックが動くことです。
何故ロジックが動くことが重要かというと、キャッシュを行うというのは本来危険なことで、静的コンテンツならともかく動的コンテンツに対してキャッシュを行う場合は高度な制御(=ロジックが書ける)が必要です。
もちろんキャッシュ用途だけではなく高度なfailoverなどにも使えます。
そういう点では、VarnishのVCL、まだ途上ではありますがNginxのnginScriptはそれに使えると考えています。またCDNでいえばFastlyにも注目しています。
またこのロジックから最適化の仕方をより自由に選べるようになると思っています。

以上を纏めると

  • データはどんどんクライアントの近くに置かれるようになる
  • 中間経路(CDN等)はよりインテリジェンスになりロジックが動くようになる。
  • 経路で実行可能なロジックはキャッシュを効率的に行うためのもの(よりサーバサイド)と透過的にクライアント向けのコードの最適化を行う(よりクライアント向け)二種類がありそう。
  • 2つは併用することでより効果を発揮する

と考えています。

新しいミドルウェアや方法論は誰のための銀の弾丸か

確かトーク中に銀の弾丸というキーワードを出したと思うのですが、誤解を招きやすいキーワードなのでここで説明したいと思います。
進歩の激しいこの業界ですから新しいミドルウェア・フレームワークが出てきては消えていっています。
出たてのモノはすごくピカピカでまるで銀の弾丸のようにみえます。
ある意味これは正しくて、もともと銀の弾丸とは狼男とかを倒せる的な意味で、熊や竜も倒せるかというとそうではなく、何にでも効くというわけではないのです。
じゃぁ狼男とはなにかというと開発した会社で抱えている問題で、必ずしもあなたの問題ではないということです。
一言で言うならばワークロードが違うわけです。
僕がここで言いたいのは新しいものを見つけた際にいきなり飛びつくのではなく、何を解決したくてそれが生まれたのか、そのワークロードは?というのを考えるべきではないかということです。

よりManaged・複合化へ

例えばHadoopを使いたい・・・けど自社では運用管理出来ないというので様々なManagedなサービスがありますが、今後も様々なミドルウェアでその動きが出て来ると思います。
そしてそれら単機能のサービスを組み合わせた複合サービスというのがでてくると考えています(AWSのMobilehubもそのようなものと考えています)
それはまるで次世代機能のサービスパックのような感じになるかなと思います。
言い方はいやらしいとは思うのですが、イノベーター理論の各グループそれぞれで次世代は違い、ラガードに行くに従ってどんどんManaged/複合化されていく(より小規模でも使える)と考えています。

個人的なパフォーマンスについての思い

僕は毎年エイプリルフールにすごく憤っています。(Twのログみてみたんですがやっぱ憤ってました)
理由は負荷が来るのがわかっていても何故対策をしないのかということです。
お金の問題というのもあると思いますが、恐らくここになんらかのソリューションが出てくると考えています。
また、パフォーマンス関連で新しいモノが出てきた際にそれだけをやれば良いということではなく基本的なことも忘れないといいなと思います。
以前記事で書いたように割と基本的なことができていないことが多かったりします。
インターネットは歴史の積み重ねでできています。その中でパフォーマンスについてもそうといえます。
もちろん陳腐化して使わないようなもの(HTTP/2であればCSSスプライトなどのように)もありますが、対策の6割以上は過去のナレッジが使えるんじゃないかなと思っています。
ぜひ次世代だけでなく積み重ねについても考えてみるといいんじゃないかなーとか思ったりしてます

最後に

運営の方々登壇者の方々参加者の方々、server_perfセッションのmirakuiさん・cubicdaiyaさん、そしてJxckさん、皆様本当にお疲れ様でした。
他のセッションやセッション後、打ち上げでの話など非常に楽しかったです!

#や っ と ぼ く の n e x t w e b c o n f が お わ っ た ぞ !


 Posted by at 2:12 AM
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


5月 302015
 

ここ最近いくつかのサイトを見ていて、アレ?妙に重くない?とDevTools等を見てみたらいろいろな問題点を見つけました。
例えばベースページが重いというのもあるのですが、単純にリソースが大きすぎる、ヘッダがおかしい等少しの工夫で閲覧をする人たちは快適になるだろうというのを思いました。
正直なところ今回記述する内容はいろんなサイトや書籍では触れられてはいるのですが、サイトを見回って共通で考慮が漏れていて、余りサイトに変更を加えずに効果がでそうなのを纏めてみました。

そのページを見るのにどれだけのダウンロードが必要ですか?

最近はPC環境もモバイル環境もより強力になり、リッチなコンテンツをストレスなく見ることが可能になりました。
マシンスペックやブラウザの高速化等いろいろありますが、ラストワンマイルのNW帯域が改善したのが大きいと個人的には感じています。
しかし、それに甘えて不必要なリソースをダウンロードさせてないでしょうか?
最近のChromeはLTEや3GなどのNW環境をエミュレーションしてどのように表示され、どの程度時間がかかるかなどがわかるので早速チェックしていきましょう。(もちろん実機が一番なので簡易的手段です)

iPhone6(LTE)偽装 11.82sec 4.8MB

iPhone6(LTE)偽装 11.82sec 4.8MB

例えばこのサイトはスマホページで4.8MBもダウンロードしています。
通信環境の悪いところで快適に閲覧できるようなサイトでしょうか?

iPhone6(LTE)偽装 2.8min 83.6MB

iPhone6(LTE)偽装 2.8min 83.6MB

このページに関しては13回見るとキャリアの制限に達するというそもそも論外なレベルです。

PC(FTTH) 25.06sec 11.2MB

PC(FTTH) 25.06sec 11.2MB

これはとあるキュレーションサイトのトップページなのですが、PC環境ならいいのでしょうか?

例えばお絵かきSNSとして有名なPixivですが
PCのマイページを見たところ4.07sec 3.2MBで、iPhone6(LTE)偽装で3.58sec 1.1MBでした。
画像が主役でダウンロードサイズが大きいだろうと予想されるサイトでもこの程度です。

今回僕が見た問題のあるサイトの多くは不必要に画像のサイズが大きかったり、そもそもクライアントキャッシュが効かなかったりしていました。
失礼ながら開発する際に実機ではあまり見てないように思えます。(見ていたとしてもwifi接続でとか)
重いサイトをでも耐えて見たくなるようなコンテンツがあるような場合や遅くても体感速度は速い場合は違うかもしれませんが、普通に考えて頻繁に見ようとは思わないと思います。
もう使い古されたような話(2006年)ですが、Amazonでは0.1秒遅くなると売上が1%減少するというのもあります。
また、これらの多くのサイトはクラウドで構築されており、多くの場合転送課金が発生しています。
純粋にコスト的観点からでもいいですし、ユーザからの視点でもいいので、まずは自分のサイトを認識するところからはじめましょう。

リソースのリクエスト数を減らそう(INM/IMS)

リソースを見ているとCache-Control/Expiresがついていないため
同じリソースを毎回更新されていないかとサーバに問い合わせしているケースが有りました。
そのページでしか利用しない画像ならともかくCSSやjsの用に複数のページで利用されるものもこうだとそれだけでレンダリングが悪化します。

304 Not Modifiedいっぱい

304 Not Modifiedいっぱい

クライアント側がリソースを既に持っている場合はIf-None-Match(INM)もしくはIf-Modified-Since(IMS)リクエストというのを発行して更新があるかをチェックします。
(ちなみにINMはETagをIMSはLast-Modifiedが含む場合に動作し、その両方がなくcache-control/Expiresも無い場合は毎回レスポンスを取得します)
更新がない場合はキャッシュされたリソースを使用しますがそれでもサーバにリクエストする分時間がかかりますし、サーバの負荷やリクエスト課金があるような場合はコスト増にもつながります。
そのためCache-Control/Expires/ETag/Last-Modifiedは適切に指定するべきです。(参考:HTTPキャッシュの作成
ちなみにCache-Controlだけでも良いのではと思う人もいるかもしれませんが、ETag/Last-Modifiedがあれば期限が切れた際にINM/IMSリクエストが行われリソースに更新が無ければキャッシュをそのまま使うため不必要なダウンロードが発生しなくなります。

TTLの目安
今まで毎回リクエストが飛んでいたところを、じゃあクライアント側でキャッシュを使えるようにしようとあまり考えずに長めにTTLを設定すると問題が起きるかもしれません。
簡単にいうとファイルを更新したのに(当然ですが)クライアント側のキャッシュが変わらないということです。
そのためファイル名にバージョン情報や日付などをつけたりして異なるURLにしてしまうと解決ができます。(ex: hoge.js?20150526)
これは非常に強力ですがテンプレートの修正などが必要になるためすぐに適用できないことも考えられます。
とはいってもTTLを短くしてしまってはあまり意味がありません。
じゃぁどの程度のTTLがあれば良いのでしょうか?
一つの目安ですがサイトの滞在時間を目安とするとよいでしょう。

GoogleAnalyticsより例

GoogleAnalyticsより例

見てる間に不要なダウンロードが発生しなければ良いのでそれに合わせてしまえば比較的短く、そして効果的なクライアントキャッシュが出来るでしょう。
もちろんこれは小手先の対応で最終的にはURLを変える対応を行ってTTLは長めに設定するのが良いです。

そのETag適切ですか?

とあるサイトを見ていると304に混じって毎回何故かダウンロードしているものを見つけました。
そこで原因を調べてみるとファイルが更新されていないのにETagが変わるのを発見しました。

ハッシュ値が同じなのにETagが違うファイル

ハッシュ値が同じなのにETagが違うファイル

先ほどのリソースのリクエスト数を減らそうでも触れましたがINMではキャッシュで保持しているETagをサーバにリクエストして更新があればダウンロードを行うものです。
そのためETagが変われば更新されているものとしてダウンロードがされてしまいます。

同じファイルなのにETagが違うためボディを取得した例

同じファイルなのにETagが違うためボディを取得した例

このサイトの場合はjs/cssで起きていて、なおかつcache-controlがなかったのでページ遷移をする度に運が悪いとこれらのファイルが読み込まれていました(運がよいと304)
ではなぜこういう現象が起きるかというと、大体の場合はサーバ側の設定不備かDeployの問題に分けられると思います。
ApacheのETagのデフォルト値はINode-MTime-Sizeです(参照)
単一サーバで配信する場合はこれで問題は起きないのですが、複数サーバの場合だとサーバ間でINodeが同一でないためここがずれます。
そのためファイルとしては同一であってもETagが変わってしまいます。
これを防ぐにはINodeを使わなければOKです。
次にdeployの問題ですが、単純に複数のサーバに配ってる間に時間がずれてしまうためおきます(MTime部分がズレる)
例えばdeployにrsyncを利用しているのであれば-tオプションを利用してタイムスタンプを維持するようにするとよいでしょう。

コンテンツのgzip圧縮転送を有効にしよう

例えばjquery.js(1.11.2)の場合、gzipの有無でこれだけ違います(確かこの数値はApacheかNginxのデフォルトで取ったので圧縮率上げればもっと行くはず)

gzip Size Per
Off 95,952B(94KB) 100%
On 33,287B(33KB) 34.7%

画像などの既に圧縮されているようなものには効かないですがCSSやjs等のテキスト系には非常に効果的です。(もちろんベースページも)
テキスト系のものには適用できるように設定するとよいでしょう

2015/11/02追記
AmazonS3にcssやjsを直接置いているサイトが結構ありますが、gzipの考慮がもれているように思われます。
個人的には画像とかはともかくcss/jsをS3から直接配信するのはオススメしません。(オリジンとして置くには良いと思います)
最近jsで1MBを転送しているサイトを見かけまして少しきになりましたので追記しました。

画像のクオリティを調整しよう

JPGは画像のクオリティ(以下q)を調節することでサイズを大幅に小さくすることが可能です。(PNGも圧縮率等調整することで多少小さくすることは可能)
当然ながらqが低ければ画質は悪化しますがその分小さくなります。

q=100の画像のクオリティを下げていった場合のファイルサイズ

q=100の画像のクオリティを下げていった場合のファイルサイズ


しかしq=90ぐらい落としてもwebで使う分には十分な画質が有ります。
q=100

q=100 / 368KB(オリジナル 焼肉奢ってくれる人募集しています)


q=90

q=90 / 122KB


q=80

q=80 / 81.2KB


q=70

q=70 / 63.6KB


q=1

q=1 / 6.4KB

どれ位サイズが小さくなるかは画像によって違いますし
どの程度のqが必要かはサイトの特性に対する考慮、その他ベンチマークをする必要はありますが普通は90もあれば十分綺麗です。80でもよく見なければ気づきにくいとおもいます。
また当然のことなのですが元々qが低い画像のqを上げた場合も無意味です。

q=1のものを100にした画像 (30.8KB)

q=1のものを100にした画像 (30.8KB)

もしサイト画像のqが既に90以下で一括で90にあげようとした場合は逆にサイズが増えるでのできちんと調べておくと良いです。

画像のサイズを適切にしよう / サムネを作ろう

当然ですがサイズが大きい画像はファイルサイズが大きいです。
適切なサイズ・クオリティで作成することでページは軽くなります。
先ほどの画像の元サイズを100%として10%刻みでサイズを落としていくとこうなります(q=100のまま)

10%刻みで小さくしてみた

10%刻みで小さくしてみた


なるだけ表示サイズに合わせて画像を生成するべきでしょう。
動的にサイズを指定してサムネイルを作成するMWもあるのでそれとキャッシュを組み合わせるなどしてもよいでしょう。

その写真は本当にPNG(可逆圧縮)が必要ですか?

先ほどの写真(JPG/q=100)をPNG(24bit)に変換するとサイズが大抵の場合膨れます。

JPG(q=100 667×500) PNG(24bit 667×500)
313,885B(306KB) 597,334B(583KB)

もちろん8bit(256色以下)であれば小さくはなります。

PNG8(256Color) / 186KB

PNG8(256Color) / 186KB


ですが個人的にはqを下げたほうが良いと思います。
もちろんアイコン等のtooltipはPNG8が良いでしょう。

PageSpeed Insightsを活用しよう


Google先生が割と教えてくれます。
とりあえずかけてみて参考にするとよいでしょう。

ApacheのMPMの違いとリバースプロキシでのキャッシュ

ApacheでPHPを使う場合大体mod_phpを利用すると思いますがこれはPreforkで動作します。
Preforkは1プロセスが1リクエストを捌くものです。
プロセスはそれぞれで独立したメモリ空間を持つためスレッドで動くworkerに比べ同時に捌けるリクエスト数は少ないです(workerは1スレッド1リクエスト)
これがなんの問題があるかというと画像等の静的リソースをPrefoekで動いているサーバでレスポンスしてしまうことです。
通常、ベースページに紐づく静的リソースは多いため大量のリクエストが来ます、静的なので高速にレスポンス出来ると思いますが無駄にサーバリソースを使っているのは確かです。
そのため可能であれば静的リソースはworkerなサーバでレスポンスして、動的コンテンツのみをpreforkなサーバで返したいところです。
ドメインを分けるのも手ですが、それが難しいばあいはリバースプロキシを入れるのも手です。
リバースプロキシで静的リソースをキャッシュしてしまいpreforkなオリジンサーバにリクエストがあまり来ないようにすればよいでしょう。
また、これは通信環境が悪くて遅いクライアントの影響を小さくする対する対策にもなります。
リバースプロキシのMWはNginxやApacheやVarnishを使うと良いと思います。僕はVarnishが好きです。

その動的コンテンツは更新されますか?

サイトにもよると思いますが、リクエストしても毎回同じコンテンツを返す場合はリバースプロキシでキャッシュすることを検討するのもよいでしょう。
高コストなLLでページを生成する必要がなくなるため、突然buzzっても割と耐えられるようになります。
また全体でサーバコストを減らすことも可能でしょう。
何かしら情報を取得するためにLLを動かす必要があるというのであれば、それはjsで代用ができないかとか検討するのもよいでしょう(もしくはESIを検討するのもよいでしょう)
動的コンテンツをキャッシュすることで一気に軽く、そしてコストを減らせるでしょう。

サーバ情報を隠そう

パフォーマンスとは関係ありませんが
単純に危ないので不要な情報(特にバージョン情報)は消すようにしましょう。

phpのバージョンまで出てる

phpのバージョンまで出てる

HTTPS(SSL/TLS)の設定を確認しよう

少し前に話題になったPOODLEHeartbleedなど(直近だとLogjamやFREAK)HTTPSに関わる脆弱性をよく聞きます。
いろいろあるものの今自分のサイトの状態がわからないのであればとりあえずGlobalSignQUALYS SSL Labsの診断をやってみましょう。
GlobalSignの診断はSSL Labsのを元にしているのですが日本語なので分かりやすいです。
とりあえず指摘事項を見つつサーバ設定を直していくと良いでしょう。(セキュリティだけではなくパフォーマンスに影響する項目もあります)

SSL使用をアピールしてるものの設定は・・・

SSL使用をアピールしてるものの設定は・・・


SSL Labsのほうだとどのような環境で見れるかも確認できるので両方みると良いと思います。
SSL Labsは環境のシミュレーションをやってくれる

SSL Labsは環境のシミュレーションをやってくれる


古いAndroidやガラケーをサポートする必要がある場合などサポートするべき環境などによって設定は変わるのでAを取るのは難しいのですが可能な限り不用意な設定等は排除したいものです。

そのVaryヘッダ本当に大丈夫ですか?(2017/02/17追記)

CDNやリバプロ等のクライアント・サーバ間でキャッシュを行う場合きちんと考えて設定したいVaryというヘッダがあります。
多くの場合はVary: Accept-Encodingが指定されていることが多いです。
このヘッダが意味するところは
Accept-Encodingによって内容が変わるので、中間でキャッシュする場合はクライアントから送られるAccept-Encodingヘッダ毎にキャッシュ持ってねということです。
cssやjsはgzip圧縮して転送したりしますが、gzipに対応していないクライアントも存在します。
もし、Varyを指定してない状態で、先にgzip圧縮されたcssが中間でキャッシュされ、gzip非対応のクライアントでリクエストをするとgzipなcssがレスポンスされて困ってしまいます。
そのためVaryを使うことでこのURLはAccept-Encodingで内容が変化することを宣言しているわけです。

なるほど、これは便利だということでUser-Agent(以下UA)を指定をするサイトもあるようです。
おそらくPC・スマホ・ガラケーでも同一URLで処理したいということなのかなと思いつつ
これは非常にもったいない設定です。
なぜならUser-Agentは非常に多いため、キャッシュのヒット率が低下します。(もちろんパフォーマンスも)
わかりやすいところだと、AndroidのUAは機種名が入ってたりします。
つまりその機種毎・そしてバージョン毎に個別のキャッシュが作られてしまいます。
もちろん意図したものであれば問題ないのですが、大抵の場合はなんとなく設定されているのでは考えてしまうものが多いです。

上記は割と大きな音楽・本の配信サイトなんです。見たところ画像をパラメータによって動的にリサイズしておりそれをキャッシュしようとしていますがVaryにUAを指定しています。
当然なのですがUAを変えてリクエストをしたところMissになりました。
パラメータで生成条件を変えているためおそらくUAでユニークである必要はないと考えられ、そもそも外すか(画像は既に圧縮されてるのでgzipがあまり効かないためそのまま送ることが多い)Accept-Encoding指定でよいのではと思います。
ちなみに、CDNやミドルウェアによってはこのあたりを賢く制御(PC/スマホで個別にキャッシュを持つ)などもできたりします。
CDNやキャッシュを使う場合は、Varyに気をつけてみてください。

(小ネタ)クライアントとCDN/リバースプロキシでのキャッシュの違い

さて、クライアントでのキャッシュとCDN/リバースプロキシでのキャッシュは何が違うのでしょうか?
すごく簡単にいうとそのキャッシュが参照されるクライアント数が1なのか多なのかだと考えています。
当然ながらクライアントに格納されているキャッシュは他のクライアントに参照されません。
そのため極端なことを言ってしまえば、サイトがほぼ100%直帰されて再訪もないのであればクライアントのキャッシュをいくら頑張っても無意味です(そんなサイト無いと思いますが)
対するCDN/リバースプロキシでのキャッシュはその場合でもうまく動きます(ちなみに/としていますがCDN=リバースプロキシと言ってるわけではないです併用するのが大事です)
両者の違いを意識してうまく併用できると良いかなと思います

まとめ

もちろんサイトの状況によっていろいろ変わると思いますが
これらの対策をやるだけで割とサイトのパフォーマンスは良くなると思います。
また、今回紹介したのは割と基本的なところで他にもいろんな高速化の方法があります。
他にも最低限気にすべきこともありますがパフォーマンスとはあまり関係ないので削りました。(なんとインターネット上にmysqlのポートを開けてるとこがありました・・)
みなさんのサイトが速くなればいいなーとおもってます。
参考になれば嬉しいです。

参考リンク

パフォーマンスの最適化
ESIの効果と気をつけた点
Varnishを多段にする利点と注意するところ
SSL証明書のインストールチェックはどのサイトを利用すべきか
SSL のパフォーマンスでお嘆きの貴兄に
Webサーバ勉強会#5に参加してきましたよ


 Posted by at 3:44 PM
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は問題なしです)
あとでチケットきろうかなと思います。


2月 082015
 

サービスをスケールさせるためにロードバランサー(以下LB)をよく使用します。
LBは大量のリクエストをノードに振り分けるは当然で、他の重要な機能の一つしてヘルスチェックがあります。
御存知の通りノードが死亡した場合に切り離しを行う機能です。
箱モノのLB(F5やA10など)やソフトウェアで行うHAProxy、Varnishなどでのヘルスチェックの「行う側」の設定方法はよく記事で見かけるのですが
ApacheやNginxなどのヘルスチェックを「受ける側」についての記事は余りないように思えたので
今回HTTPでのヘルスチェックを小ネタとして書こうと思います。
(別にMySQLのヘルスチェックでも考え方はそんなに変わらないです)
全般的に私としての考えなので、人によっては違うかもしれないです。参考程度にどうぞ。

ヘルスチェックの設計

ヘルスチェックを設計する場合に気をつけることはたった一つで
「そのヘルスチェックでどこの範囲の正常性を担保するか」ということです。

h1
Apache上でPHPが動いているAppノードを例で考えてみましょう(すごくざっくりな図です)
※この記事ではノード=VMインスタンスや物理的なサーバ、サーバ=Apacheなどとして書いています

静的ファイルでのチェック

よくやるのは/healthcheck/check.htmlのような静的なファイルを置いてLBからヘルスチェックを行う方法です。
これでチェックが出来る範囲について考えてみましょう。

  1. LB<->Appノード間のネットワーク
  2. Appノードのネットワークの状態(ソケットを使い切ってないか等)
  3. サーバ(Apache)が起動しているか
  4. ファイルが存在して取得できるかどうか

ざっくりコレぐらいチェック出来ます。
しかしこれではPHPが動くかどうかのチェックはできません。

動的ファイルでのチェック(1)

単純にPHPの動作をチェックするということで


<?php
echo "ok";

というPHPでチェックします。
これにより更に

  • PHPが動作するか

のチェックが追加で可能になります。
簡単なチェックではありますが、PHPが動く事をある程度保証できます。
ここである程度と言っているのは使用しているextensionはチェックしていないからです。
私は過去にapc_fetchでのspinしていることを見たことが有りますがこれではチェックできません。
どこまでチェックできるのか、それをヘルスチェックでチェックするのかについて把握出来るようにしましょう。

動的ファイルでのチェック(2)

サービスページのトップページなどをヘルスチェックの対象としてみたケースを考えてみましょう。
ここでチェック出来るのはトップページから呼ばれるDBやKVSも含めたサービスの一部分です。
ヘルスチェックとしては比較的網羅的で素敵に見えます。
しかし、例えばDBが高負荷で一時的に応答が不可能になった場合について考えてみましょう。
その場合すべてのノードがLBのヘルスチェックにFailする可能性があります。
ここで重要なのがAppノードは正常なのにです。

一体何をチェックするのか

先ほどの例でトップページをチェックした場合は他のノード(DB等)の影響でFailすることがあることを説明しました。
これはヘルスチェックとして適切かというとLBから行う「サービス」のヘルスチェックという点ではある程度適切ですが「ノード」のヘルスチェックという点では不適切です。
逆に単純なコードでのチェックや静的ファイルでのチェックは「ノード」のヘルスチェックという点では適切ですが「サービス」のヘルスチェックという点では不適切です。
では「サービス」のチェックを行いたいためにトップページをヘルスチェックするのは正しいことでしょうか?
これはどのような考えでヘルスチェックを行うかによって変わります。
要はLBでなにを行いたいのかということです。
最初に書いたとおり多くの場合は大量のリクエストの負荷分散を行うために複数のノードを纏めてクラスタとしています。
ここで提供/保証したいのはサービスの継続性ではなくクラスタの継続性です。
つまりLBが行うべきなのは負荷分散を行い、異常を示したノードを切り離してクラスタを継続させることです。
複数の仕事を一つでやろうとすると複雑になったり、考慮漏れがあったりするのでそれは監視などにわけてもいいでしょう。

h2

ではLBでサービスの継続性を目的としてはいけないかというとそうではありません。
ただ、非常に難しいのです。
ヘルスチェクの設計で重要な点として述べた「そのヘルスチェックでどこの範囲の正常性を担保するか」ということで考えてみましょう。
ノード単位であればすべてを網羅出来るわけではないものの疎通が出来る、LISTENしているなど比較的少ない要素でほぼ正しく動く状態ということを担保でき比較的わかりやすいです。
しかしサービスは非常に多くの機能から構成されています。ログイン、投稿、課金などなど一体全体どの範囲をチェックするのかという問題が出てきます。
LBですべてのチェックを行う事は事実上不可能です。(極端な事を言えば数秒毎にJenkins叩いて正常性確認できるでしょうか?)
もちろんうまく範囲を定義出来て合意が取れればそれでも構いませんが、サービス全体のことも何らかの形でケアしないといけません(これについてはノード単位でも同じですが程度の問題です)
というかLBで完結する必要はなく監視システムと連携してアラートが上がったらキルスイッチでそのサービスをメンテ・Sorryページに飛ばすということでしょう。
そのキルスイッチを持つのがLBなのかサービス側なのかそれとも両方なのかは設計次第となります。

小ネタ:高負荷でのSorryページについて
高負荷になった時にSorryページを出すのは非常に魅力的です。
しかし高負荷になった場合に一律でSorryを出してもよいものでしょうか?
例えばECサイトで決済をしようとしているユーザ・カートに商品を入れているユーザ・単純にページを見ているユーザを一律で切ってもいいのかという話です。
これはヘルスチェックを「する側」・「受ける側」、そしてサービス側での設計が必要ですが、Sorryに飛ばすのは単純にページを見ているユーザだけにとどめて
決済をしようとしているユーザやカートに商品を入れているユーザは通過させるということも可能です。(識別子をつけておく)

単一ノードに複数のサーバがあって関連する場合

h4
単一のノードに例えばApacheだけではなくlocalのmemcachedをたてるケースが存在します。
この場合はどうするべきでしょうか?
これも結局どの範囲を保証するかを考えるだけですみます。
memcachedが動作していることが必須であればヘルスチェックのコードでmemcachedのstatsを叩くコードも含めればよいでしょう。
逆にコード側でlocalのmemcachedが死んでいた場合でも別に問い合わせをするなどで致命的な問題を引き起こさないとかケア出来てるのであればなくてもよいでしょう。(監視からの障害対応などでの対応)
クラスタ内のノード数が少なく、なるだけノードを切り離したくない+サーバが落ちても別に問い合わせするなどして影響が無いのであればそれでも良いと思います。
ざっくり言うと、複数ノードに跨るヘルスチェックは辞めたほうがよく、単一ノードで複数のサーバがある場合はそれを通しでチェックするのは良いと考えています。

Proxyのヘルスチェック

h3
LVSとProxyを組み合わせて上記のような構成を取ることが有ります。(LB-L7がProxyです)
その場合LVS-ProxyとProxy-Appノードの2つです。
Proxy-Appノード間は今まで紹介してきた通りなのですが、LVS-Proxyは注意が必要です。
簡単にいうとProxyはリクエストを中継するためにあるのでヘルスチェックのリクエストもノードのクラスタに飛ばしてしまうことがあることです。
そうしてしまうと複数のノードを跨るヘルスチェックとなってしまうのと、特定のクラスタのヘルスチェックの結果に引きづられてしまいます。
大抵ヘルスチェックはIPアドレス指定で行うので、複数のクラスタ定義がある場合はミドルウェアによりますがだいたい先頭の定義に振り分けられるからです。
そのためRewriteでヘルスチェックのURLであればそのまま200を返すのがよいでしょう。
ここでもう一つ注意なのがRewriteでヘルスチェックの結果を返すようなことをAppノードでは避けたほうがいいということです。
わざわざ書いているのはApacheはProxyにもWebサーバにもなり、ヘルスチェックの設定を流用してしまって思わぬ事故につながることもあるからです。
きちんと範囲を意識して、そのロールに適切なヘルスチェックの設定なのかを考えるべきです。

ヘルスチェックと障害調査

「そのヘルスチェックでどこの範囲の正常性を担保するか」ということをある程度把握しておくと障害調査の時に楽ができます。
何故と言うと障害調査を行う場合にヘルスチェックによってそこまでの切り分けができるからです。
例えばサービスがダウンしていて、そのサービスが乗っているクラスタはヘルスチェックによって正常であるならば少なくともLB-ノード間は異常が無いのであろうとわかります。
逆に把握ができていない場合はそれらも障害切り分けの時に考慮する必要がありますし、ヘルスチェックの設計自体に問題がある場合はそもそもそこが障害ポイントともなり、調査に時間がかかる原因になりかねません。

まとめ

ヘルスチェックを「受ける」側のことについて書いてみました。
単純に静的ファイルを置く、コードを置く、RewriteでOKするなど様々な返し方がありますが
間違った使い方をすると意図せず切り離されたり、あれ?切り離されていない?といった事になります。
そのヘルスチェックで何をチェック出来るのかを考えて設計するのがよいと思います。
またヘルスチェックは万能ではなく他のシステム(特に監視など)と補完の関係にあると考えています。
ヘルスチェックは機械的に判断できる要素を設定して、監視は他の判断や複数の要素(ノードやサービスのコード等)に跨るでもよいかなと思います。
うまく使うことでサービス全体の可用性を向上できるといいなーと思います。