nginx・リバースプロキシについて

## 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を利用することでスレッドプールを利用し、パフォーマンスを向上することができる）ため、１つのワーカープロセスが使用できる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から学ぶ高速化の実践](https://gihyo.jp/book/2022/978-4-297-12846-3)
- [［24時間365日］サーバ/インフラを支える技術](https://gihyo.jp/book/2008/978-4-7741-3566-3)
    - [5分で【サーバ/インフラを支える技術】を読んだ気になる - Qiita](https://qiita.com/r548/items/b9ed5ea9212450546c99)
- [nginx実践入門](https://gihyo.jp/book/2016/978-4-7741-7866-0)
- [NginxとApacheって何が違うの？？ - Qiita](https://qiita.com/hiroaki-u/items/f2455d62f8a4017663cb)
    - [Re: NginxとApacheって何が違うの？？](https://blog.inductor.me/entry/2022/05/31/150707)
- [Nginxのアーキテクチャを理解する - Qiita](https://qiita.com/kamihork/items/296ee689a8d48c2bebcd)

## Unicorn & nginxのサンプル

nginx.conf

```nginx
# upstream name: バックエンドサーバーの論理グループを定義するブロック。
# app-serverという名前のサーバーグループを定義し、proxy_passのURLとして利用する。
upstream app-server {
  # unicornで設定したUNIXドメインソケットのpathを設定する
  # TCP/IPポートではなくUNIXドメインソケットを使うことで、同一マシン内の通信をより効率的に行う
  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リクエストを1つのTCP接続で効率的に処理するために、TCP接続を維持する（keepalive）が、クライアントとのTCP接続がアイドル状態になってから切断されるまでの秒数。

  root /app/current/public; # 静的ファイルのルートパス

  # location path: 特定のパスへのリクエストの処理。
  # /（ルートパス、全パス）へのリクエストの処理を定義する。
  location / {
    # try_files: 指定した順序でファイルを探索する。
    # リクエストされたURIのパスに対応するファイル（静的コンテンツ）が存在すれば、そのファイル返す。
    # 存在しなければ、名前付きロケーション `@unicorn` に内部リダイレクトする。
    try_files $uri @unicorn;
  }

  # /assets から始まるパスへのリクエストの処理を定義する。
  location ~ ^/assets/ {
    # 静的ファイルのルートディレクトリを明示的に指定
    # リクエストされたURIのパスと root パスを組み合わせて、最終的なアクセス先を提供する。
    # 例: /asssets/image.jpg にリクエストした時、`/app/current/public/assets/image.jpg` のファイルを返す。
    root /app/current/public;
  }

  # 名前付きロケーションへのリクエストの処理（内部リダイレクト）を定義する
  location @unicorn {
    # リクエストを転送する先となるアップストリームサーバーのURLを指定する（upstream ディレクティブで定義したもの app-server）。
    proxy_pass http://app-server;
    # proxy_set_header: リクエストを転送する先に渡すHTTPヘッダーを設定する。
    # X-Forwarded-Proto: 元のリクエストで使用されたプロトコル（http または https）を転送先に伝えるためのヘッダー
    proxy_set_header X-Forwarded-Proto $scheme;
    # X-Forwarded-For: クライアント（エンドユーザー）のIPアドレスと、経由したプロキシ（ELBなど）のIPアドレスのチェーン
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    # Host: リクエストのホスト名を転送先にも伝える（デフォルトではproxy_passで指定したホスト名になる）
    proxy_set_header Host $host; 
    # X-Real-IP: 最終クライアント（エンドユーザー）の実際のIPアドレスを転送先に伝える
    proxy_set_header X-Real-IP $remote_addr;
  }

  # error_page: 指定したHTTPステータスコードが発生した時の表示ページを設定する
  # 500 502 503 504 が発生した時は、/500.html を表示する。
  error_page 500 502 503 504  /500.html;

  # /500.html に完全一致でリクエストした時の処理を定義する。
  location = /500.html {
    # リクエストされたURIのパスと root パスを組み合わせて、最終的なアクセス先を提供する。
    # 例: /500.html にリクエストした時、`/app/current/public/500.html` のファイルを返す。  
    root /app/current/public;
  }

  # access_log path: HTTPリクエストのログの格納先ファイルの指定
  access_log /var/log/nginx/access.log;
  # error_log path: Nginxの内部エラーや警告のログの格納先ファイルの指定
  error_log /var/log/nginx/error.log;
}
```

[Nginx のリバースプロキシ設定のメモ - Qiita](https://qiita.com/HeRo/items/7063b86b5e8a2efde0f4)
[Capistrano3 + Rails4 + Unicorn + Nginx + EC2でサーバー構築！ - Qiita](https://qiita.com/SanoHiroshi/items/d7942d66678f0d60f0ed)
[Capistrano3 + Rails4 + Unicorn + Nginx + EC2でサーバー構築！ - GitHub](https://gist.github.com/SanoHiroshi/99e437d926f9f3e6297e)
[3.1 Nginxの基本構文](http://www2.matsue-ct.ac.jp/home/kanayama/text/nginx/node16.html)
[8.3.1 server](http://www2.matsue-ct.ac.jp/home/kanayama/text/nginx/node78.html)
[nginx はどのようにリクエストを処理するか](https://tengine.taobao.org/nginx_docs/ja/docs/http/request_processing.html)
[nginx連載5回目: nginxの設定、その3 - locationディレクティブ](https://heartbeats.jp/hbblog/2012/04/nginx05.html)
[kenjiskywalker/nginx_try_files_memo.md](https://gist.github.com/kenjiskywalker/4596258)
[remote_addrとかx-forwarded-forとかx-real-ipとか - Carpe Diem](https://christina04.hatenablog.com/entry/2016/10/25/190000)
[docker-compose+Nginx+Rails6 で静的コンテンツをNginxから配信する - Zenn](https://zenn.dev/junki555/articles/4848fe33909741ff811b)
[DockerでRails環境(Nginx+Unicorn+MySQL)を構築してFargateへデプロイするまで〜開発環境編〜 - Qiita](https://qiita.com/GuChiM1292/items/b7c54ae1b0f831060a70)
[UnicornとNginxの接続方法は、UNIXドメインソケットとリバースプロキシの2つの方法がある - kasei_sanのブログ](https://blog.kasei-san.com/entry/2020/09/02/175828)
[【Rails】WEB/APサーバ構成に応じたNginx設定 - Qiita](https://qiita.com/NaokiIshimura/items/7cb2390243939a34754f)
[作って覚えるリバースプロキシ - 東北ギーク](http://tech.respect-pal.jp/reverse_proxy_cooking/)
[Nginx/OpenRestyあるある言いたい - Usual Software Engineer](https://innossh.hatenablog.com/entry/2017/08/22/223300)

## 自分が過去書いた関連記事

- [プロセスとスレッドについて（Ruby・JavaScript・Goの例付き） - Qiita](https://qiita.com/yokoto/items/3b4bcc489d93bc57baf3)
- [Web3層構造と、Webサーバー・アプリケーションサーバー・Railsの関係 - Qiita](https://qiita.com/yokoto/items/50e1728f3e3d11fd1527)