nginxとは
- nginxはC10K問題を解決するために開発されたWebサーバー。
- リバースプロキシとして利用される代表的なソフトウェア。
- nginxでは設定ファイルを書くことで、以下のようなことが可能になる。
- リバースプロキシとしてアップストリームに指定したアプリケーションサーバーにリクエストを送る
- 直接静的ファイルを配信する
- 設定ファイル上ではifを使った簡単なロジックを利用できる。
- 複雑なロジックは利用できない。
C10K問題とは
- マルチプロセス・シングルスレッドのアーキテクチャにおいて、同時に大量のリクエストが来る場合、CPUが処理をするプロセスを切り替えるためにコンテキストスイッチ(≒ CPU上のキャッシュの切り替え)が大量に発生し、クライアント数が1万を超えた辺りでパフォーマンスが極端に落ちること。
- nginxもマルチプロセス・シングルスレッドのアーキテクチャだが、イベント駆動というアプローチによってC10K問題を解決している。
マルチプロセス・シングルスレッド
- クライアントからの1リクエストを1プロセスが処理を行なっているアーキテクチャのこと。
- PHP
- unicornを利用したRubyで動いているアプリケーションサーバー
シングルプロセス・マルチスレッド
- 同じプロセス上のメモリ空間をスレッド同士が共有できるため、使用するメモリが少なく済むアーキテクチャ。
- スレッドを切り替える時もマルチプロセス・シングルスレッドのアーキテクチャと同様にコンテキストスイッチが発生するため、1スレッドがレスポンスを返すまで占有されるアーキテクチャにするとC10K問題が発生する。
Goのgoroutine
-
軽量スレッドと呼ばれる仕組み。
- スレッドよりも低コストに並行処理を実装できる。
- C10K問題を回避する手法の1つ。
- Goのランタイムがプログラム起動時にCPUのコア数分のスレッドを生成する(環境変数GOMAXPROCSなどを使用することで変更可能)。
- Goのランタイムはm個のgoroutineをn個のスレッドで実行するためにm:nスケジューリングと呼ばれる手法を用いた独自のスケジューラを実装しており、そのスケジューラが各goroutineの実行タイミングや実行するスレッドを決定する。
- そのため、Goのgoroutineを利用してアプリケーションを実装すれば、スレッドのことを意識せずに効率よく並行処理を実装できる。
並行処理について
- 並行処理を実装するには考えることが非常に多い。
- たとえば、並行処理では同じメモリに格納されている変数に対して同時に参照と書き込みを行った場合の動作は保証されないし、書いたコードの実行順番も保証されない。
- そのため、並行処理を実装する場合は並行処理についてよく学ぶ必要がある。
- 並行処理を実装すると、どうしてもプログラム自体が複雑になってしまい、扱えるプログラマーも少なくなってしまう。
リバースプロキシとは
- Webアプリケーションの前段でユーザーのリクエストを受け取り、代表的には以下の役割を果たす。
- 負荷分散(ロードバランス)
- コンテンツのキャッシュ(HTTPリクエストの内容に応じたシステムの動作の制御)
- HTTPS通信の終端
- HTTPヘッダー(URL)の書き換え
- IPアドレスを用いたアクセス制御
- User-Agentによるアクセス制御
- アクセスログを利用したリクエストのロギング
- HTTPのKeep-Alive
負荷分散(ロードバランス)
- ユーザーのリクエストをまずはリバースプロキシで受け付けた後に、アップストリームサーバーとして指定したアプリケーションサーバーに分散してリクエストを送ることで、大量のリクエスト・レスポンスを処理できるシステムが構築できる。
- Webサービスへのアクセスが増えてアプリケーションサーバーの負荷が問題になった場合、アップストリームサーバーに指定するサーバー台数を増やすことでアプリケーションサーバー1台当たりのアクセス量を減らすことができる。
- 逆に、一部のサーバーにリクエストを偏らせることもできるため、リクエストを送るサーバーを柔軟に制御できる。
コンテンツのキャッシュ(HTTPリクエストの内容に応じたシステムの動作の制御)
- IPVSはL4なので、クライアントから要求されたHTTPリクエストの内容に応じて処理を振り分けるようなことはできない。
- ここでリバースプロキシがあると、たとえばHTTPリクエストの中からURLを見て、最終的な処理をそれぞれ別のサーバーに振り分けるような制御が可能になる。
- クライアントから要求されたURLが/images/logo.jpなら画像用のサーバーに
- クライアントから要求されたURLが/newsであれば動的コンテンツを生成するサーバーに
HTTPS通信の終端
- ユーザーと直接通信をするサーバー上でやる方がパフォーマンス上有利。
HTTPヘッダー(URL)の書き換え
- 昨今ではサイト全体の階層構造をイメージしやすいなどの理由から、ユーザーに対してWebサイトのURLを綺麗に見せたいこともある。
- 「クールなURI」を実現するには本来Webアプリケーション側で対応するべきだが、レガシーなシステムをどうしても利用せざるを得ない場合がある。
- そんな時はリバースプロキシでリクエストURLを分解してから、レガシーシステムが理解できるURLに変更してサーバーへ転送するというのも1つの手。
IPアドレスを用いたアクセス制御
- クライアントのIPアドレスを見て特定のIPアドレスのみサーバーへのアクセスを許可することができる。
- 悪意のあるホストからのリクエストを遮断する目的に利用できる。
- 管理者向けのページが含まれるサイトでIPアドレスとURLによる制御を組み合わせて、管理者向けページには特定のIPアドレスからのみしかアクセスできないように制御することもできる。
User-Agentによるアクセス制御
- クライアントのUser-Agentを見て、任意のUser-Agentからのリクエストを特別なWebサーバーへアクセスを誘導する。
- GooglebotやYahoo!Slurpなどの検索エンジンのロボットへの対応などに利用できる。
- たとえば、ユーザー向けにはどうしてもキャッシュすることが難しい動的なページ(ユーザーに合わせてユーザー名が表示されるページなど)があるとする。
- ロボットにはユーザー名を表示する必要がない場合、そのページをキャッシュすることができる。
- そこでUser-Agentを見て、ロボットのUser-Agentの場合はキャッシュサーバーを利用してアプリケーションサーバーへアクセスさせるよう制御を行う、といったことが可能。
アクセスログを利用したリクエストのロギング
- Webサーバーのアクセスログにアクセスを受けたURLと、リクエストを受信してからレスポンスを返却するまでに経過した時間を記録することで、レスポンスタイムを集計することができる。
- 各URLのレスポンスタイム(レイテンシ)は、Webサービスの性能を測る数値として、最初の指標となる。
参照: 達人が教えるWebパフォーマンスチューニング〜ISUCONから学ぶ高速化の実践 3-2 負荷試験の準備
HTTPのKeep-Alive
- クライアントととリバースプロキシの間のみ「Keep-Aliveオン」にして、リバースプロキシとバックエンドのアプリケーションサーバー間は「Keep-Aliveオフ」にすることで、接続の維持をメモリ消費量の少ないリバースプロキシが担当し、メモリ消費量の多いアプリケーションサーバーでは(プロセス数が少なかったとしても)1リクエストが終了するとすぐその直後に別のリクエストに応答できる。
リバースプロキシを利用するメリット
- クライアントとの通信は前段のリバースプロキシが行い、アプリケーションサーバーは前段のリバースプロキシとの通信のみを行える。
- 遅いクライアントにレスポンスを返す際もリバースプロキシがレスポンスを返してくれるため、遅いクライアントとの通信でアプリケーションサーバーのプロセスが占有されなくなる。
- アプリケーションサーバーは、安定した通信ができるリバースプロキシに対してレスポンスを返せばよい。
- 遅いクライアントにレスポンスを返す際もリバースプロキシがレスポンスを返してくれるため、遅いクライアントとの通信でアプリケーションサーバーのプロセスが占有されなくなる。
- 少数のリバースプロキシから多数のアプリケーションサーバーにロードバランスすることで、大量のリクエストを受け付けられるシステムを構築できる。
- 画像・CSS・JavaScriptなどは静的ファイルであることが多く、静的ファイルに対してはアプリケーションサーバーで配信するのではなくリバースプロキシで直接静的ファイルを返すことでパフォーマンスが上がる。
nginxのアーキテクチャ
- マルチプロセス・シングルスレッドで動作する。
- イベント駆動のアーキテクチャを採用している。
マルチプロセス・シングルスレッド
- nginxは、マスタープロセスとその子プロセスであるワーカープロセスの2種類のプロセスが起動している。
- 親プロセス(マスタープロセス)が子プロセス(ワーカープロセス)を管理し、子プロセスが実際の処理を担当する一般的な構成。
マスタープロセス
ワーカープロセスの制御と管理をしている。
ワーカープロセス
- リクエストを受け付ける。
イベント駆動のアーキテクチャ
- 各ワーカープロセスは、クライアントからのリクエスト・レスポンスの処理をイベント駆動で並列に実行する。
- nginxはリクエスト・レスポンスに伴うI/O処理を並列かつ高速に扱うため、多重I/OやノンブロッキングI/Oを活用している。
- nginxはノンブロッキングI/Oと多重I/Oを活用し、イベント駆動でリクエスト・レスポンスを扱うことで同時並行に大量のリクエストを捌くことができる。
- この手法によりnginxはC10K問題を解決している。
- nginxはスレッドを利用していない(デフォルトでは無効だが、aioを利用することでスレッドプールを利用し、パフォーマンスを向上することができる)ため、1つのワーカープロセスが使用できるCPUは1コアのみ。
- そのため、ワーカープロセスは複数起動することが一般的。
- ワーカープロセスすうはnginxのworker_processesという設定を指定することで変更できる。
- デフォルトは1だが、autoと指定することでCPUのコア数を自動で指定できる。
- Webアプリケーションの実装において、ノンブロッキングI/Oや多重I/Oの細かい実装方法を知っている必要はないが、少し知っているだけでもトラブルシューティングなどに活用できることがある。
ノンブロッキングI/O
- 一般的にはファイルの内容を読み込む場合、プログラムはデータの到着を待つ。
- 書き込みも同様に書き込みが完了するまで待つ。
- このI/O処理が完了するまで待つ状態をブロッキングと言う。
- ノンブロッキングI/Oはこのブロッキングを避けることができる。ノンブロッキングI/OはC言語の場合、ファイルをopenする際のフラグでO_NONBLOCKを付与することで利用できる。
- ノンブロッキングI/Oを使用すればI/O操作でブロックされることがなくなる。
- その代わり処理がブロックした場合、エラーが発生するため適切にリトライする必要がある。
- C言語では、グローバル変数のerrnoにEAGAINかEWOULDBLOCK(Linuxでは同じ値)が入っている場合にリトライが必要。
多重I/O
- 多重I/OはC言語の場合、select・epoll(Linux固有)・kqueue(BSD固有)を使用することで利用できる。
- 複数のファイルディスクリプタを同時に渡すことができ、いずれかのファイルディスクリプタがI/O可能になった時に通知を受け取ることができる。
- うまく活用することで、複数のI/Oを効率よく扱うことができる。
- Linuxの場合、selectよりもepollの方がパフォーマンスが高いため、Linux上のnginxはデフォルトでepollを利用する。
※ 非同期I/O
- 多重I/O似た手法。Go言語などでよく活用される。
- 一般的にGo言語ではI/Oを通常通りブロッキングするシステムコールで呼び出す。
- システムコール呼び出しで処理がブロックされるため、ブロックされる処理をgoroutineと呼ばれる軽量スレッド経由で呼び出す。
- そのgoroutine内の処理がブロックされている間に、他のgoroutineの処理を実行することでマシンのリソースを活用できるようにしている。
- この手法は非同期I/Oと呼ばれている。
- 既に紹介したノンブロッキングI/Oや多重I/Oも、OS依存のコードになってしまうが、Go言語ではsyscallパッケージを利用することで使用できる。
- ただ、システムコールの使い方をよく知っていないと利用することは難しい。
参考
- 達人が教えるWebパフォーマンスチューニング〜ISUCONから学ぶ高速化の実践
- [24時間365日]サーバ/インフラを支える技術
- nginx実践入門
- NginxとApacheって何が違うの?? - Qiita
- Nginxのアーキテクチャを理解する - Qiita
Unicorn & nginxのサンプル
# upstream name: サーバーグループの定義。
# app-serverという名前のサーバーグループを定義し、proxy_passのURLとして利用する。
upstream app-server {
# unicornで設定したUNIXドメインソケットのpathを設定する
server unix:/tmp/app_unicorn.sock
}
# server: サーバーグループのパラメータの指定
server {
listen 80; # リクエストを80番ポートで待ち受けるようにする。
server_name example.org www.example.org; # リクエストをどのサーバー(ホスト)で処理するべきかの定義。
client_max_body_size 10m; # リクエストボディの最大許容サイズ。
keepalive_timeout 80; # HTTP通信をタイムアウトせずに待つ秒数。
root /app/current/public;
location / {
# URIのパスに対するファイル(静的コンテンツ)が存在すれば、そのファイル返す。
# 存在しなければ、動的コンテンツとして@unicornに内部リダイレクトする。
try_files $uri @unicorn;
}
# 静的ファイルの配信を(アプリケーションサーバーの代わりに)nginxで行う
location ~ ^/assets/ {
root /app/current/public;
}
location @unicorn {
proxy_pass http://app-server; # アップストリームサーバー(転送先のサーバー)のURLの指定。
proxy_set_header X-Forwarded-Proto $scheme; # プロキシ(ELBなど)のプロトコル(HTTPまたは HTTPS)を特定・設定する。
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # X-Forwarded-For ヘッダーに直前のProxyを追加
proxy_set_header Host $host; # Host ヘッダーにこのサーバーを表すホスト名を追加(デフォルトではproxy_passで指定したホスト名になる)
proxy_set_header X-Real-IP $remote_addr; # X-Real-IP ヘッダーにリクエストの送信元IPアドレスを追加
}
# 500エラーページ
error_page 500 502 503 504 /500.html;
location = /500.html {
root /app/current/public;
}
# ログの格納先の設定
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
}
Nginx のリバースプロキシ設定のメモ - Qiita
Capistrano3 + Rails4 + Unicorn + Nginx + EC2でサーバー構築! - Qiita
Capistrano3 + Rails4 + Unicorn + Nginx + EC2でサーバー構築! - GitHub
3.1 Nginxの基本構文
8.3.1 server
nginx はどのようにリクエストを処理するか
nginx連載5回目: nginxの設定、その3 - locationディレクティブ
kenjiskywalker/nginx_try_files_memo.md
remote_addrとかx-forwarded-forとかx-real-ipとか - Carpe Diem
docker-compose+Nginx+Rails6 で静的コンテンツをNginxから配信する - Zenn
DockerでRails環境(Nginx+Unicorn+MySQL)を構築してFargateへデプロイするまで〜開発環境編〜 - Qiita
UnicornとNginxの接続方法は、UNIXドメインソケットとリバースプロキシの2つの方法がある - kasei_sanのブログ
【Rails】WEB/APサーバ構成に応じたNginx設定 - Qiita
作って覚えるリバースプロキシ - 東北ギーク
Nginx/OpenRestyあるある言いたい - Usual Software Engineer