ぱいそんにっき

The limits of my language are the limits of my world.

nginx を知る:: 後編

後編です

前回 [1] に引き続き, nginx について残り学んだことを書いていきます. 調べるのにめちゃ時間がかかっていたところを, めっちゃ丁寧にわかりやすく教えてもらいました. 本当にありがとございます!!! 正しく理解できているかの自己チェックも兼ねて教わったことを書き残していこうと思います.

さて, 前編で web サーバを構築する為にマシンに nginx のインストールとその設定ファイルの解読, そして OS のしくみを少し齧りながらサーバがクライアントとどのような通信をしているのかを学びました. 今更当たり前なことを書きますが, サーバというのはクライアントに何かのサービスやコンテンツを提供するソフトウェアのことなのですね. そしてそのクライアントとの情報の受送信は, web サービスならばほとんどは HTTP 通信を使っている... ということまでをやっと分かりました(汗) 細胞間のシグナル伝達系と似ていて興味深いです.

では今日の本題. サーバはクライアントの要求に対して応答するソフトウェアです. そしてクライアントから見たらサーバというのはリクエストの受付とレスポンスの返信さえしてくれたら満足で, サーバ内がどんな実装をしてどう動いているかは抽象化できることが分かりました. クライアントが望む通信さえしていたら, サーバは目的の為にどんな設計でどんな通信, どんな言語で実装してもよいということを前提の認識で進めていきます.

今回はクライアントとの通信をし, レスポンスのコンテンツを返す別のアプリケーションサーバを使ったサーバ構成での nginx のしくみを見ていきます. クライアントから大量のアクセスを同時に早く効率よく捌く為にアプリケーションサーバへのアクセスを減らす手段のひとつとしてリバースプロキシというサーバ構成を学びました.

リバースプロキシとしての nginx

前編で学んだように, 通信がひとつ発生するたびに Connection ひとつを消費しプロセスがその処理をする必要があるので, その分マシンのメモリと CPU を消費することになります. そのため, 同時に大量のクライアントからアクセス(通信のリクエスト)が来る場合サーバはそれに応じた大量のメモリ・CPU を消費することによってマシン負荷がかかってしまいます. この負荷を削減するアプローチとして nginx をリバースプロキシとして使う方法があります.

今回想定しているサーバ構成です.

../../../_images/012.png

サーバの global IP を xx.xx.xx.xx とし, クライアントからのリクエストは 80 番ポートを通して xx.xx.xx.xx:80 宛に nginx に届くとします. また, サーバ内にもう一つデーモンとして走っているアプリケーションは 127.0.0.1:8000 として常時起動しており, 外部のサーバからはアクセスできませんが同じサーバ内の nginx はこのアドレス宛に通信ができるとします.

図を見ると, クライアントからの要求に対してレスポンスをするアプリケーションの前に nginx をリバースプロキシとして置くことで, クライアントからの大量のリクエストを肩代わりしてアプリケーションに来るリクエストの負荷を軽減させるのですね.

リバースプロキシの用途は負荷を分散させる他にもセキュリティ向上, 暗号化/SSL 高速化, 速度調整などのいろいろな用途があるそうで [2] , 目的に合わせて適切に設計して行かねばです.

nginx の設定

nginx のリバースプロキシの設定を見ながらどのような設定をしているのか見ていきます.

server {
    listen       80;
    server_name  0.0.0.0;

    location ~* <アプリケーションの URI> {

        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

順に見ていきます. まず, nginx がバーチャルホストの 0.0.0.0 かつ 80 ポートでアクセスしてきたリクエストを受け付けます. そのリクエストの URI が指定のものと一致した場合, location {...} 内の設定に書かれた処理が進みます.

この location {...} 内に書かれている 4 行がアプリケーションに通信をつなぐリバースプロキシの設定箇所です. 用語を整理しつつ順に設定の内容を見ていきます.

バーチャルホスト

サーバで複数のドメインを運用する(クライアントがサーバに対してリクエストを送るホスト名を基にして応答するホストを決定する)ときのホスト名

0.0.0.0

default root. ホスト自身を指すアドレスとして定義されソースアドレスと(パケットの送信元)してのみ利用される. マシンが持っている全ての IP を示す, 外部の IP からでも接続できる.

127.0.0.1

loop back address. localhost と同じ意味. マシン自体を表すアドレス. 同じマシン内部のプログラム間で通信したい場合に使う.

proxy_pass

プロキシ先のURI の指定をする. 今回はクライアントからの要求を同じマシン内の http://127.0.0.1:8000 へ HTTP 通信を使ってリクエストを渡している.

proxy_set_header

proxy_pass の設定によってクライアントからの要求であるリクエスト内容(リクエストヘッダ情報, URI)を全てプロキシ先に送るが, その送るリクエストヘッダに新たにパラメータを追加する時に使う. proxy_set_header A B と定義することでアプリケーションへ渡すリクエストヘッダに A: B を追加できる. B は文字列または nginx 固有の変数を使える.

proxy_set_header Host $host

リクエスト先の Host を $host(クライアントが送るリクエストヘッダのホスト名, つまり今回は xx.xx.xx.xx となる.) これによって, Host: 127.0.0.1:8000 から Host:xx.xx.xx.xx にヘッダが書き換わる.

proxy_set_header X-Real-IP $remote_addr

本当のリクエスト元の IP を示す X-Real-IP に $remote_addr(クライアントのアドレス)を追記.

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for

本当のクライアントの IP を特定するための X-Forwarded-For に $proxy_add_x_forwarded_for(クライアントからのリクエストヘッダの X-Forward-For の値, もし持ってなかったら $remote_add を入れる) を追記.

この設定によって各々の通信とそのヘッダ情報が以下のように変わります.

../../../_images/022.png

X-Real-IP, X-Forwarded-For の 2 つは慣例的につけるヘッダのようです. このヘッダを追加しておくことでクライアントのアドレスを特定することができるので, アプリケーション側でリクエスト元のアドレスを確認したり, クライアントのアドレスによって処理を変えたい場合などに適応できそうです.

HTTP access control (CORS) ヘッダ

リバースプロキシがどんな通信をしているのか見えてきたところで次はヘッダの設定を見ていきます. HTTP 通信においてヘッダというのはクライアントからの要求を受ける時に, 要求された URI の他にも必要なクライアントの情報を扱うコンテンツとは別の情報なのだそうです. [3]

つまり, クライアントから同じ URI でリクエストが来たとしてもそのヘッダ情報によって, 返す処理を変更できる設定ができるのですね. nginx でその処理ができればアプリケーションに無駄な通信を渡さなくて済みますね. そして, サーバ側でクライアントからのリクエストを制御することでより安全な通信を保証することもできます.

ヘッダにはいろいろな種類があるみたいですが [4] 上の設定に関わる, 異なるドメインアドレスからのリクエストを判別する CORS ヘッダ [5] について書いていきます.

再び設定ファイルを見ていきます.

location ~* <アプリケーションの URI> {

    if ($request_method = OPTIONS ) {
        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Methods "POST, GET, OPTIONS";
        add_header Access-Control-Allow-Headers "Origin, Authorization, Accept";
        add_header Access-Control-Allow-Credentials true;
        add_header Content-Length 0;
        add_header Content-Type text/plain;
        return 200;
    }

    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods "POST, GET, OPTIONS";
    add_header Access-Control-Allow-Headers "Origin, Authorization, Accept";
    add_header Access-Control-Allow-Credentials true;
}

add_header A B と記述することで, クライアントからのリクエストに対して返すレスポンスヘッダに A: B を追加することができます. 順に見ていきます.

add_header Access-Control-Allow-Origin *;

このサーバにリクエストを送ることを許可する URI を指定します. * の場合は全ての URI からのリクエストを受け付けることを意味しています. 特定のクライアント(例えば http://hoge.com)からのみリクエストを受け付けたいときには http://hoge.com のように, 指定します.

add_header Access-Control-Allow-Methods “POST, GET, OPTIONS”;

クライアントがサーバへアクセス時に許可するメソッドを指定します. この場合は POST, GET, OPTIONS のみなので, PUT や DELETE などのリクエストの場合は受け付けずに status code 405 を返します.

add_header Access-Control-Allow-Headers “Origin, Authorization, Accept”;

クライアントがサーバに対して使用できるヘッダを記述します. この場合は, クライアントは Origin, Authorization, Acceptというヘッダを追記してリクエストを送ることができます. 例えば Basic 認証が必要な時に, Access-Control-Allow-Headers に Authorization を追記しておかないとクライアントから認証が通らない場合があるのでその都度適切なヘッダ情報を指定しておく必要があります.

add_header Access-Control-Allow-Credentials true;

クライアントのリクエストに対してレスポンスを示すかの真偽値.

通信時には URI だけじゃなくヘッダ情報によって処理を制御できるのですね. ヘッダの大切さがだんだん分かってきました!

キャッシュの設定

さて, 最後はキャッシュの設定です. nginx をプロキシサーバとして立てることでクライアントからのアクセスを肩代わりしてリクエスト内容によってバックエンドのアプリケーションへ渡すレスポンスを絞り込むことで, 負荷を軽減させ・通信の安全性を上げるところまでわかりました. さらに, nginx のキャッシュの設定をすることで, ある一定時間内での同じリクエスト内容によって同じレスポンスを返すようにし, アプリケーションへの通信コストを減らすことができるそうです.

ではではまた設定ファイルを見ていきます.

http {

    proxy_cache_path /var/cache/nginx/cache levels=1 keys_zone=zone1:10m inactive=10m max_size=100m;

    location ~* <アプリケーションの URI> {

        proxy_cache zone1;
        proxy_cache_key $proxy_host$uri$args;
        proxy_cache_valid 10s;
    }
}

キャッシュの設定をする proxy_cache_path について詳しく見ていきます. [6]

/var/cache/nginx/cache

キャッシュファイルの保存先の指定

levels=1

キャッシュに使うサブディレクトリの階層の数. 1 だと /var/cache/nginx/cache/xx/aaaa みたいに保存される.

keys_zone=zone1:10m

キャッシュに使うゾーンの名前とその容量の指定. ゾーンとは, nginx のプロセスでメモリ上にキャッシュを管理する領域がありそこを示す名前. このゾーンにどのくらいまでキャッシュするかの容量を指定する. この場合は zone1 に 10M 使う.

inactive=10m

指定した時間の間アクセスがないとキャッシュを削除する. この場合はキャッシュの有効時間は 10 分間.

max_size=100m

指定したパスに実際にキャッシュして保存するファイルの最大容量の指定.
../../../_images/031.png

location {...} での設定でも同様に使うゾーン名やキャッシュの有効秒数などを指定しています.

通信の全体像

これで教わったことをひと通りまとめ終わりました. 最後に通信の全体像を図にかいて終わります.

../../../_images/04.png
  1. クライアントからリクエストを受け付けた nginx のプロセスはヘッダと URI の情報からバックエンドで走っているアプリケーションにプロキシするかの判断をします.
    1. でリバースプロキシのリクエストを送る必要がある場合は, もうひとつ Connection を開いてアプリケーションと HTTP 通信によって通信を送ります.
    1. でリバースプロキシした通信のレスポンスを受け取ってキャッシュし, クライアントにレスポンスを返します.
    1. でキャッシュ有効時間内であった場合は保存していたキャッシュファイルをレスポンスとして返します. このときバックエンドサーバとの通信は行われません.

なるほど!!!! 理解するとものすごくすっきりしました. 今回は HTTP レイヤーでの通信に着目しました. 上の通信は TCP レイヤーやその下のレイヤーも動いていますが, nginx を知るという目的において全体を俯瞰して見るときには HTTP のレイヤーを考えればよいのですね. そして, 「いまどのレイヤーにいるのか」を起点とした上で通信の設計をしていくという基本がやっと分かりました.

細胞間の伝達経路について見ているならそのレイヤーだけに着目すればよくて, その下のレイヤー, 酵素反応経路や化学変化, 原子移動については裂いて考える... レイヤーを裂いて何が主体と客体かをしっかり抑えた上で考えるのはどの分野でも共通することなのを改めて感じました. レイヤーという抽象の壁 [7] の考え方はプログラミングだけじゃなくてメタに考える上で何にでも適応できるのですね!

コンピュータのことが未知すぎて俯瞰することもままならないレベルだったせいか, 今回知らない用語を片っ端から調べていくという非効率な方法でしか理解のアプローチができず, 時間や労力をかけたわりに目的が掴めないままおわって無駄なことをしていたのが大反省点です... 一度理解できたら早いらしいので, 次の課題は未知要素が多い中でどうやって自力で全体を俯瞰してレイヤー区分できるようにするかです... いい方法を考えねば(汗)

とはいえ基礎の基礎以前になんも知識がなかったところからようやくここまで追くことができました(・・;) サーバエンジニアにはまだほど遠〜〜〜いですが, いつか一緒に仕事させてもらえるようがんばります.

タイトルの通り, nginx を知るという目的はクリアできた気がするので(まだまだ知るべきことはたくさんありますが)一旦ここで閉めます. 教えてくださって本当にありがとうございました!!!(^^)