エムスリーキャリアでエンジニアをしている山本です。プライベートでは日本酒とそれに合うおつまみを検討していることが多いです。最近は「ポテトサラダ」と「仙禽 雪だるま」の組み合わせが良かったです。
この記事では「なぜ Rails アプリケーション(Unicornで動作)の手前に Nginx を設置するのか」という疑問に対してIOの観点から自分の考えている意義について紹介したいと思います。
対象読者
- Rails + Nginx 構成のアプリケーションと関わったことがあり、なぜわざわざ Nginx. を置くのか疑問に思っている方
- 知識としてはぼんやり分かっているのだが、検証の上納得したいと思っている方
経緯
- 業務の中で Nginx → Rails(Unincorn)構成のサービスに関わる機会があった
- よくある構成なのだが「直列に接続されたシステムなので稼働率という点ではRails(Unicorn)単体のほうが好ましいのでは」とは思っていた
- ここについて質問されたときになにか用意できる答えが欲しいと一念発起した
3行まとめ
- Unicornが「ブロッキングI/O」に対し Nginxは「ノンブロッキングI/O」という性質を持つ
- クライアントの通信速度が遅い場合、Unicornは他クライアントへのリクエストに答えられない場合がある
- 一方でNginxは遅いクライアントへのレスポンスを待たず次のクライアントへのリクエストを処理できる点で優れている
Unicornが通信が遅いクライアントに対して弱い理由
ブロッキングI/Oとは「入出力を行っている最中はそれが終わるまで他の処理を進めない」入出力処理のことです。
例えばクライアント2人がUnicornへリクエストした場合を考えます。その場合は以下のような順に処理されます。
- クライアントAがUnicornへ処理に5秒かかるようなリクエストを行う
- 1の直後にクライアントBが処理に3秒かかるようなリクエストを行う
- Unicornは5秒かけてクライアントAにレスポンスを返す
- その後3秒かけてクライアントBにレスポンスを返す
この場合、「クライアントBはリクエストしてから8秒後にレスポンスが返っています」。
このように通信が遅いクライアントがいた場合、そのクライアントへの通信が完了するまでの間他のクライアントへのレスポンスが滞ってしまうためUnicornは通信が遅いクライアントがいた場合効率が悪いとされています。
NginxがUnicornに対して異なる点
NginxはノンブロッキングI/Oで並行してクライアントからのリクエストを処理できる点が異なると言えます。
ノンブロッキングI/Oは「処理の完了を待たずに他の処理を行える」ような性質のことです。
そのため上述した低速な通信のクライアントがリクエストされた場合でも、それと並行して処理を進められます。
例えば 2人のクライアント -> Nginx -> Unicorn の状況における通信について考えたとき、以下のような図になります。
上記の良い点は「Unicornはクライアント側の通信品質に左右されない」点です。
Nginxは低速なクライアントを相手にしていても並行して他のクライアントからのリクエストを受付けることができます。
UnicornはNginxと通信することだけに集中するだけでよく、レスポンスを返したらすぐに他のクライアントのリクエストを処理することができます。
低速なクライアントが原因で他のリクエスト処理が遅延する心配がありません。
検証
上述の内容は既に様々な記事で語られているかと思います。以降では実際に自分が検証した内容などを記載していきます。検証内容から自身が理解していたことの解像度や納得感が高まれば幸いです。
今回は検証のためにdocker-composeファイルを用意しました。
構成の概観は下記です。
以上の構成に対しホストOSからリクエストを行い検証を行いました。
以降では docker-compose up でコンテナが立ち上がっている前提で説明を進めます。
(低速クライアント, 普通のクライアント) -> Unicornへ直接リクエスト
低速クライアントを擬似的に再現する手段を検討したところ「telnetでコネクション張りっぱなしにする」ことにしました。理由はどうであれ「コネクションが閉じない」事象を再現すれば良いと判断しています。
低速(疑似)、普通の順番でリクエストを行ったとき、「低速なクライアントのレスポンスが返った後に普通のリクエストのレスポンスが返る」ことを想定しています。
ターミナルで複数タブを開き、一方のタブでは下記を実行します
date; telnet localhost 8080; date
その後に一方のタブでは下記を実行します
date; curl localhost:8080; date
その後telnet側のタブにて下記を実行しました
# telnet の接続内で下記を実行
GET / # エンターでレスポンスが返る。
結果としては下記のようになりました
# telnet 側の出力
2022年 12月 7日 水曜日 19時17分13秒 JST
...レスポンス内容...
2022年 12月 7日 水曜日 19時18分15秒 JST
# curl 側の出力
2022年 12月 7日 水曜日 19時17分24秒 JST
...レスポンス内容...
2022年 12月 7日 水曜日 19時18分16秒 JST
以上より、「telnet側のレスポンスが返った直後にcurl側のレスポンスが返った」と判断できます。
(低速クライアント, 普通のクライアント) -> Nginx -> Unicorn な経路でのリクエスト
前述の手順を Nginx へ向けて実行してみます
# telnet 側
date; telnet localhost 80; date
# curl 側
date; curl localhost; date
結果は下記です
# telnet 側
2022年 12月 7日 水曜日 19時19分38秒 JST
...レスポンス内容...
2022年 12月 7日 水曜日 19時19分47秒 JST
# curl 側
2022年 12月 7日 水曜日 19時19分43秒 JST
...レスポンス内容...
2022年 12月 7日 水曜日 19時19分43秒 JST
以上からは「telnet側の通信完了を待たずcurl側の通信が完了している」と判断できました。
結論
以上より、「UnicornはブロッキングI/Oな特性から低速なクライアントが原因で他のクライアントまでレスポンスが遅くなりがち」と言えるかと思います。
その点から「Unicornにはリクエストの処理に集中してもらい、クライアントにレスポンスを返す部分はNginxが肩代わりする」ようなところに意義があるのだろうと考えました。
もちろん「worker_processesを調整すれば良いのでは」という見方もあります。
実際にサービスでの利用を検討する場合は様々な観点がありますが、この記事が「Unicorn, Nginxの特性を把握した上で最適な構成を検討する一助になれば」と思いました。
読んでいただいてありがとうございました。
追伸:低速なクライアントをローカルで再現させるために色々調べたことも「参考」に記しておきます。この調査と検証が一番手こずりました。とほほ。
さらに追伸:社内にて下記のような指摘をいただきました
- nginx と unicorn を比較しているようだが、それぞれ httpサーバ・アプリケーションサーバと分類が違うもの同士で比較するのはいかがなものか
- クライアントからの通信状況に左右されない理由として「ノンブロッキングIO」を挙げているが、「リバースプロキシ」としての特性が理由なのでは?
- puma の存在に触れると良いのでは?
暖かいご指摘ありがとうございます。
この記事の作成とフィードバックを通して諸々整理できました。
「本記事の記載内容のピントの合ってない感じ」含め楽しく読んでいただくと共に、頂いた指摘を踏まえて「理解していたことが整理される」きっかけとなれば幸いです。