えいのうにっき

a-knowの日記です

「nginx 実践入門」を読んだ

先日発売された nginx 実践入門を読みながら、おー、なるほど、と思ったところをメモもしていたので、それを読書メモとして晒す。ちなみに当方、nginx 初心者。(見よう見まねで conf を書いたことがある、くらい。)

nginx実践入門 (WEB+DB PRESS plus)

nginx実践入門 (WEB+DB PRESS plus)

なるほどポイント

nginx 起動時に sudo が必要な理由

nginx は、初期設定では 80 番ポートを bind した状態で、デーモンとして起動するんだけど、一般ユーザでは 1024 番未満のポートを bind できないので。

設定ファイルの include の注意点

include sites-enabled/*.conf などとして設定ファイルを include したとき、そのファイルの内容は include ディレクティブを記述した場所にそのまま読み込まれる。なので、location ディレクティブのブロック内に記述した場合、location コンテキストとして扱われることになる。

バーチャルサーバで使用するホスト名について

server_name ディレクティブで指定したホスト名とアクセス時のドメインが一致している」から、そのバーチャルサーバにアクセスが振り分けられる、のではない!

複数のバーチャルサーバを定義している場合、nginx は以下の優先順位でどのバーチャルサーバを使用するかを決定している。

  1. listen ディレクティブのアドレスとポートに一致するバーチャルサーバ
  2. リクエストの Host ヘッダが server_name ディレクティブで指定したホストに一致したバーチャルサーバ
  3. デフォルトサーバ

振り分けられるのは、これに合致しているから。それだけ。

ちょうど自分のサーバ(趣味運用)も複数ドメインをひとつのサーバで運用していて、なんとなく、「 server_name で振り分けられるんだろうな」と思っていたけど、上記の説明で腑に落ちた。

...が、読み進めていくと、「HTTPS 通信の場合は、通信のすべてが暗号化されているため、暗号化を解除するまで Host ヘッダの内容を知ることはできません」「このため、どのバーチャルサーバの設定を使用すればよいか server_name ディレクティブのマッチングを解決できず、1台のサーバで複数ドメインHTTPS を実現できないという問題が」...という記述が。

これまたちょうど、自分のサーバで運用しているドメインのいずれも、Let's Encrypt を用いた HTTPS 化をしている。...が、上記のような問題は起きておらず。 「なんでや?!」と思いながらさらに読み進めていくと、SNI という、「暗号化されている HTTPS 通信においてリクエストするホスト名を送信先サーバに伝えるための仕様」が、僕が用いている nginx のバージョンではデフォルトで有効になっているため、問題が起きていなかった。ということがわかった。

こんなかんじで、いろんな点が繋がる瞬間、きもちいいぞ。

worker_processes ディレクティブには auto が指定できる

auto を指定すると、CPU のコア数を自動検出しコア数と同じ数のワーカプロセスを起動できる。

sendfile ディレクティブの効果

これを有効にすると、ファイルの読み込みとレスポンス送信に sendfile() システムコールが使われるようになる。sendfile() システムコールを使用すると、ファイルをオープンしているファイルディスクリプタから直接ファイルを送信するため、効率のよいファイル送信ができるようになる。

これと合わせて、 tcp_nopush ディレクティブを有効にすることで、最も大きなパケットサイズでレスポンスヘッダとファイルの内容を送信でき、送信するパケット数を最小化できる。

location ディレクティブについて

  • location ディレクティブで使用できる修飾子の評価の優先度は、高いものから順に =(完全一致) ~(正規表現) ~*(正規表現・大文字小文字の区別なし) (なし・前方一致) ^~(前方一致)
  • 前方一致ディレクティブが複数記述された場合の location ディレクティブは、記述した順番に関わらず、文字数が長い(より厳密な指定の)記述が優先される
  • ^~ 修飾子の設定にマッチした場合は、それ以降の location ディレクティブが評価されず、 ^~ 修飾子の設定が用いられる
  • location ディレクティブをネストすることで、優先順位をよりわかりやすくすることができる

HTTP/2 や SPDY の nginx での設定について

listen 443 ssl http2 listen 443 ssl spdy とするだけでいい、ということを知った。もうちょっと面倒くさいものかと思ってた。。

リクエストボディに関する設定

POST メソッドなど大きなサイズのリクエストボディを扱うこともある場合、リクエストボディは nginx によってバッファリングされるため、いくつかのパラメータを調整しておく必要がある。

client_max_body_size

リクエストボディの最大サイズを指定。

client_body_buffer_size

nginx がリクエストボディの読み込みに利用するメモリバッファのサイズを指定する。実際のサイズがここで指定したバッファ容量を超えた場合、nginx は一時ファイルにバッファを出力するため、ディスク I/O が発生しリクエスト処理時間が長くなってしまうことがある。

client_body_temp_path

上記バッファの書き出し先を指定する。出力先デバイスを tmpfs にすることで、ディスクの I/O 待ちを発生させなくすることができる。

Unicorn と nginx との組み合わせについて

Unicorn は nginx とは異なり、イベント駆動型のアーキテクチャを持っていない。そのため、ワーカープロセスが一度に処理できるプロセス数も一つだけ。つまり、同時に処理可能なアクセス数=ワーカープロセス数、となってしまう。

そんな Unicorn と nginx を組み合わせる = Unicorn は nginx とだけやりとりをするだけでよくなった、となった場合、nginx はユーザーとのやりとりを全てバッファリングしてくれるため、Unicorn の各ワーカープロセスがブロックされる時間は、

  • 純粋に処理中のとき
  • nginx とのやりとりをしている時間

...だけになる。つまり、スループットが向上する。

また、アプリケーションでの処理が必要ない静的ファイルも nginx だけで直接配信できるようにすることでも、Unicorn のワーカープロセスを消費することを低減できる。

内部リダイレクトについて

try_files ディレクティブと rewrite ディレクティブは内部リダイレクトで処理される。内部リダイレクトでは、ロケーションを書き換えた上でもう一度そのリクエストの処理を行う、という挙動をするため、リクエストが処理された状態は引き継がれない。

proxy_cache_pathproxy_cache_valid について(疑問)

proxy_cache_path ディレクティブで指定できる inactive=1d などによる有効期限の指定だと、「指定した有効期限より長くリクエストされなかったらキャッシュ削除」となるんだけど、これを「1日経ったら問答無用で削除」みたいな挙動にしたい場合ってどうするんだろう。

少し調べてみた結果、本にも書いてある proxy_cache_valid ディレクティブでやりたいことはできそうなかんじだろうか?

たとえば、

proxy_cache_path /var/hoge/fuga/cache inactive=1h;
proxy_cache_valid 200 2h;

このように設定していた場合、早ければ(キャッシュへのアクセスが1時間、無ければ)1h でキャッシュが削除され、長くても 2h (正常アクセスの場合)で削除される。という理解でよいのだろうか。

これとは別に、キャッシュパージャー(purger)というキャッシュ削除の仕組みもあるようなのだけど、これは商用有料ライセンスのときのみ使えるもののようす?

nginx のキャッシュの仕組み

nginx のキャッシュの仕組みは、下記のようになっている。

  • キャッシュするレスポンスをいったん一時ファイルに保存
  • その後、指定したキャッシュ保存先にファイルを移動(rename)する

なので、「一時ファイルの保存先」と「キャッシュファイルの保存先」が違うデバイスだった場合、ファイルのコピーが必要となり、ディスク I/O が発生してしまう。これを回避するため、 proxy_temp_path ディレクティブを使って一時ファイルの保存先も指定するようにする。

キャッシュをコントロールするレスポンスヘッダ

nginx のキャッシュ機能では、

  1. X-Accel-Expires
  2. Expires
  3. Cache-Control

の優先順位でキャッシュの有効期限が評価される。

このうち、ブラウザのローカルキャッシュを左右するのは Expires Cache-Control のふたつ。またこれらは HTTP/1.1 で定義されていて広く使われている。

このふたつのヘッダを nginx で追加するには、 expires ディレクティブを使う。 expires 60s; としたら、expires ヘッダだけでなく Cache-Control: max-age=60 も追加される。仮にこの状態だと、nginx でキャッシュしているファイルの有効期限は60秒だし、ブラウザのローカルキャッシュの有効期限も60秒になる。

expires -1s; などとしたら、Cache-Control は Cache-Control: no-cache となる。この状態だと、ブラウザはキャッシュを用いず、毎回サーバに条件付きリクエストで問い合わせ・サーバはコンテンツの更新有無を判定した上でレスポンスを返す。となる。

つまり、 expires Cache-Control は、nginx のキャッシュとブラウザのローカルキャッシュの両方を制御することができるもの、になる。

一方で X-Accel-Expires は nginx のキャッシュのみの制御になるので、「ブラウザのローカルキャッシュの有効期限」<「nginx のキャッシュ」にしたい場合は X-Accel-Expires ヘッダを指定する必要がある。( expires だけだと、どう頑張っても「ブラウザのローカルキャッシュの有効期限」=「nginx のキャッシュ」にしかできない。)

画像サムネイルの作成

こんなことも、nginx で行わせることができるのかー!(それだけ)

Lua による拡張

世間一般的には、この本にこの章が含まれていることが、その価値をよりいっそう高いものにしているようす。なんだけど、初心者な僕は「Lua」という単語すら初めて見た、というレベル。なので今回も、ひととおり読んだけれど「なるほど、設定ファイルの中にコードを書ける・拡張できる方法があるんだなー」くらいに、今回はとどめた。

でもこれほんと強力だなーと。ちょっとしたことなら、アプリケーションサーバにまで到達させずに、nginx だけで返したいレスポンスを返すことだってできちゃうよね。

などと感動していたら、OpenRestyェ...

OpenResty とは、 nginx と nginx_lua をはじめとするサードパーティモジュールをベースにした Web アプリケーションフレームワークOpenResty には memcached、Redis、MySQL といった各種サーバソフトウェアと通信するためのドライバや JSON パーサ、セッションの暗号化/復号を行うためのモジュールなど、アプリケーションを開発するうえでよく利用されるモジュールがバンドルされている ... とのことで、あぁ、未来に生きてんな、と。nginx をベースにしてるのだから、イベント駆動型の Web アプリケーションフレームワークでもあるわけか。しかもバックにいるのが nginx ということで心強いし。Test::Nginx というテストフレームワーク?もあるみたいだし。

でもどうなんだろうな、こういう流れ、今後大きなうねりとなっていくのかどうか。今々での使いドコロとしては、よほどの大規模なサービスにおいて、より素早くレスポンスできるようにするため・アプリケーションサーバをより "本業" に専念させるために、その手前にいる nginx が行える仕事の幅を広げよう、というポジションで使う、というのが現実的なのかなぁ。

感想

今年に入ってから、趣味アプリを自分で立てたサーバー(Compute Engine を使ってみている)で運用しはじめてみていて。いままで PaaS を使うことで考える機会のなかったあれやこれやを考える機会が増えてきていて、んで、これがまたそこそこおもしろくて。

nginx も、その「あれやこれや」に含まれていて、それまで "なんとなく" "手探りで" 触ってきた nginx について、一歩踏み込んだ理解をしてみようと思ったというのが、今回の購入に至ったワケ。

そういったこともあって、非常に面白かった。あっという間に読んでしまった。見よう見まねで書いた自分の conf と照らし合わせながらこの本を読むのもまた、非常によかったと思う。

あと、「あ、これは後で自分のにも設定しておきたいな」っていうことを、サーバーの chef リポジトリの issue にも随時書きながら読み進めたので、これからこの issue たちを潰しこむ作業を進めることにもワクワクしている。w

f:id:a-know:20160303140123p:plain

おまけ

  • リクエストヘッダのリファラフィールドは referrer ではなく referer なのは、HTTP 規格策定時のスペルミスによるもの
    • それに伴い、 valid_referers ディレクティブの名前も referer に。
  • 実際にディスク I/O がパフォーマンス遅延の原因になっているかを知るには、 CPU の I/O wait を確認することが重要


follow us in feedly