概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: A New Ruby Application Server: NGINX Unit
- 原文公開日: 2018/03/28
- 著者: Nate Berkopec (@nateberkopec): Railsのパフォーマンスコンサルタントです。
- 主著: The Complete Guide to Rails Performance
参考に、NGINX Unitの動画を貼っておきます。
画像は元記事からの引用です。
新しいRubyアプリサーバー「NGINX Unit」を調べてみた(翻訳)
概要: NGINX inc.は同社の新しい複数言語対応アプリサーバーであるNGINX UnitでRubyのサポートを開始しました。NGINX UnitはRubyアプリサーバーにどんな意味をもたらすのでしょうか?NGINX Unitは注目すべき製品なのでしょうか?(2057文字、10分)
Rubyistのための新しいアプリサーバーが登場しました。その名もNGINX Unitです。名前からおよそ想像がつくように、NGINX webサーバーを擁するオープンソースの営利企業として知られるNGINX社のプロジェクトです。2017年の秋に、同社はNGINX Unitプロジェクトの発足をアナウンスしました。NGINX Unitは本質的に、従来のNGINXを用いたさまざまなアプリサーバーをすべて置き換えるよう設計されたアプリサーバーです。RubyではPuma、Unicorn、Passengerが該当します1。NGINX UnitはPythonやGoやPHPやPerlでも動作します。
この包括的なアイデアは、マイクロサービスの管理をうんと楽にしてくれるものであるように見受けられます。1つのNGINX Unitプロセスは、言語の種類を問わずにいくつでもアプリを実行できます。たとえば、1つのNGINX Unitサーバーは数種類の異なるRubyアプリを実行でき、それぞれ異なるバージョンのRubyランタイムを実行できます。あるいは、RubyアプリとPythonアプリを隣り合わせで実行することもできます。こうした組み合わせはシステムリソースによってのみ制限されます。
残念なことに、「マイクロサービス」という言葉はマーケティング関連のページでバズワード(流行語)として消費されがちです2。そういう界隈では、「動的」だの「モジュラー」だの「軽量」だのといった語に「サービスメッシュ」だの「シームレス」だの「上品な(graceful)」だのといった語をちりばめています。本記事では、そうしたマーケティングの壁を突き抜けて、Rubyアプリをproduction環境で動かす私たちにとってNGINX Unitがいかなる意味を持つのかという点に迫ってみたいと思います。
NGINX Unitのアーキテクチャや独自の特徴に迫る前に、「アプリサーバー」と「webサーバー」の違いについて今一度おさらいしましょう。webサーバーは、HTTPでクライアントに接続し、静的なファイルを提供したり、HTTPが使える他のサーバーのプロキシとして両者の仲介役を果たしたりするのが普通です。アプリサーバーは、言語ランタイムを実際に起動して実行するものです。Rubyでは両者の機能が組み合わせられることがあります。たとえば、主要なRubyアプリサーバーはいずれもwebサーバーとしても機能します。しかし、NginxやApacheなど多くのwebサーバーは、アプリサーバーではありません。そしてNGINX UnitはWebサーバーであり、かつアプリサーバーでもあります。
NGINX Unitは、main
、router
、controller
、application
という4種類の異なるプロセスを実行します。application
プロセスは文字どおりのもので、Railsアプリを実行するRubyランタイムのようなものに過ぎません。router
プロセスやcontroller
プロセス、およびこれらの協調動作、およびapplication
プロセスは、NGINX Unitの振る舞いを定義します。
main
プロセスはrouter
プロセスとapplication
プロセスを作成します。やっていることは実際にこれですべてです。NGINX Unitのapplication
プロセスは動的ですが、実行するプロセスの数はいつでも変更できます。Rubyのバージョンも変更可能ですし、サーバーの実行中にまったく新しいアプリを追加することすら可能です。実行するapplication
プロセスをmain
プロセスに知らせるのが、controller
プロセスです。
main
プロセスと同様、controller
プロセスは1つしか存在しません。controller
プロセスは、「HTTP経由でJSON設定APIを公開する」ことと「router
プロセスやmain
プロセスの設定」という2つのジョブを担当します。Rubyistの皆さまにとってNGINX Unitの最も斬新かつ興味深い部分はおそらくここでしょう。設定ファイルを使うのではなく、JSONオブジェクトをcontroller
プロセスにPOST
することで振る舞いを指定します。たとえば以下のJSONファイルを設定に使うとしましょう。
{
"listeners": {
"*:3000": {
"application": "rails"
}
},
"applications": {
"rails": {
"type": "ruby",
"processes": 5,
"script": "/www/railsapp/config.ru"
}
}
}
これを次のようにNGINX Unitのcontroller
プロセスにPUT
できます(NGINX Unitサーバーが8443番ポートをリッスンしているとします)。
curl -d "myappconfig.json" -X PUT '127.0.0.1:8443'
これで新しいRubyアプリを作成します。
NGINX UnitのJSON設定オブジェクトは、listeners
とapplications
の2つに分けられます。applications
は実際に実行したいアプリ、listeners
はアプリを全世界に公開する場所です(公開するポートなど)。
アプリやリスナーの設定変更はシームレスであると考えられます。たとえば、アプリの新バージョンを「ホットデプロイ」する場合、次のように設定に新しいアプリを追加することで完了します。
{
"rails-new": {
"type": "ruby",
"processes": 5,
"script": "/www/rails-new-app/config.ru"
}
}
curl -d "mynewappconfig.json" -X PUT
続いてリスナーを新しいアプリに切り替えます。
curl -X PUT -d '"rails-new"' '127.0.0.1:8443/listeners/*:3000/application`
この遷移は(予想どおり)シームレスであり、クライアント側では気づかれません。これはPumaの「phased restart」と似ています。Pumaのphased restartでは、各ワーカープロセスは一括で再起動されるので、それまでとは別のワーカープロセスが起動してリクエストを受け付けるようになります。Pumaでは(pumactl
ユーティリティで管理される)コントロールサーバーを用いてこれを完了します。NGINX UnitはPumaの場合とは異なり、「ホットリスタート」してもリクエストを受けるアプリのバージョンが同時に2つ存在することはありません。
Puma phased restartで、アプリに6つのワーカーがあるとしましょう。phased restartの途中で、3つのワーカーが古いコードを実行し、残りの3つが新しいコードを実行します。これによって、たとえばデータベーススキーマの変更で若干問題が発生する可能性があります。NGINX Unitのリスタートは「完全に一括で」行われるので、コードのバージョンが2種類同時に実行されているとしても、リクエストを受け付けるのは常に1つのバージョンだけです。
この機能は、自社のRubyアプリをAWSなどの「デプロイを独自に管理しなければならない」サービス上で実行している方にはかなり便利に思えるかもしれません。しかし、Herokuにはpreboot systemを用いた「ホットデプロイ」機能的なものが既にあるので、Herokuユーザーには特に便利には思えないでしょう。しかしながら、両者の機能は完全に同じというわけではありません。Herokuではまったく新しい仮想サーバーを作成して全体をホットスワップしますが、NGINX Unitは単一のマシン上で単にプロセスを切り替えているだけです。しかしクライアントの立場からすれば両者はまったく同じです。
router
プロセスは、その名前から想像できる機能にかなり近い感じで、クライアントからのHTTP接続をwebアプリのプロセスに渡すものです。NGINX社は、単一のUnitルーターで数千件の同時接続を取り扱えると主張しています。router
の機能はNGINX webサーバーにかなり似ていて、サーバーへの接続の受け付け/バッファ/解析を行う多数のワーカースレッドがあります。
私の目には、RubyistたちがNGINX Unitで大いに驚嘆するであろう部分はここだと思えます。Rubyアプリサーバーでは、アプリサーバーの前面に何らかのリバースプロキシを配置しなければ(大量の)HTTP接続をさばくのは極めて難しくなります。たとえばUnicornはリクエストをバッファリングできないため、リバースプロキシの背後に配置することだけが推奨されています。つまり、もしクライアントが1バイトのリクエストを送信した後(おそらくスマホの接続状態が悪いなどのネットワークの状態が原因で)停止したとすると、Unicornのプロセスは一切の働きを止めてしまい、リクエストのバッファリングが完了するまで再開できないでしょう。たとえばUnicornの手前にNGINXを配置することで、Unicornにリクエストが到達する前の段階でNGINXがリクエストをバッファリングできるようになります。NGINXは高度に最適化されたC言語で記述されており、RubyのGVLの制約を受けないので、Unicornに変わって数百件もの接続をバッファリングできるのです。Passengerは基本的に、NGINXやApache33の単なるアドオンとして存在することでこの問題を解決しています。このソフトウェアがPassenger(mod_ruby
!)という名前で呼ばれている理由と、接続関連の作業をwebサーバーに任せている理由がこれでわかりましたね!そういうわけで、NGINX UnitはUnicornよりもPassengerに近いと言えます。
アプリの設定にはprocesses
というキーがあります。このキーには、プロセスの最小数と最大数を設定できます。
{
"rails-new": {
"type": "ruby",
"processes": {
"spare": 5
"max": 10
},
"script": "/www/rails-new-app/config.ru"
}
}
プロセスの最小数は、何らかの理由でspare
という名前で呼ばれています。上の設定では、即座に5つのプロセスが起動し、負荷に応じて最大10までスケールします。
Pumaのpreload_app!
や、PassengerやUnicornの同様の設定についての語(キー)はまだ利用できませんが、プロセスを必要が生じる前に起動しておいて、かつコピーオンライトメモリを利用することが可能になります。
これによってapplication
プロセスたちが保持されます。ここで興味深いというか驚きのポイントは、router
はapplication
プロセスとのやりとりにHTTPを使わないという点です。router
はUnixソケットと共有メモリを用いています。これはマイクロサービスアーキテクチャを念頭に置いた最適化のように思われます。というのも、同一マシン上のサービス間のやりとりでHTTPを使わなければ著しく速度が向上するからです。ただし私はこのしくみについてRubyのコード例をまだチェックしておりません。
長期的にはNGINX Unitの手前でNGINXを動かすことが意図されているのか、それともNGINX Unitはフロントに何も置かずに単体で実行してよいのかどうかについては私もまだよくわかっていません。現時点(2018年第1四半期)では、おそらくNGINX Unitの手前でNGINXをリバースプロキシとして実行しておくべきでしょう。というのも、NGINX Unitには静的ファイルサービスや、HTTPS(TLS)とHTTP/2がないからです。両者の統合はかなりシームレスになることは明らかです。
NGINX Unitは安定版の1.0リリースに近づいています。実際には、現時点でRubyアプリをproductionで実行することはできません。本記事執筆時点では、(NGINX Unitの)Rubyモジュールは文字どおりたった5日前に更新されたばかりです。現時点では極めて活発に開発が進められており、マイナーバージョンが数週間おきにリリースされています。TLSやHTTP関連の機能は、どうやら静的ファイルサービスとともに次の目玉機能になりそうな様子です。Javaのサポートについては一部で議論されており、おそらくはJRubyやTruffleRubyもその流れでサポートされるのではないでしょうか。
Windowsはサポートされていません。私はこれについては期待しないでおこうと思います。NGINX UnitでサポートされるRubyはバージョン2.0以降のみです。
本記事ではNGINX Unitのベンチマークを行うつもりはありません。NGINX UnitのRubyモジュールは極端に新しいので、おそらくベンチマークできる状態ではないでしょう。しかし、私がNGINX UnitをPuma/Unicorn/Passengerと比較するベンチマークを行わない本当の理由は、Rubyでアプリサーバーを選定する場合に問題になるのはスピード(技術的に言えばレイテンシ)ではなく、スループットの方だからです。アプリサーバーの違いは、処理の速さではなく、同時に処理可能なリクエスト数に現れる傾向があります。アプリサーバーで動かすアプリのレイテンシのオーバーヘッドは極めて小さく、おそらく数ミリセカンドのオーダーにとどまるでしょう。
Rubyアプリサーバーでスループットに影響する設定のうち、最も重要なのはスレッディングです。その理由は、アプリサーバーの設定項目のうちで、アプリがコンカレントに扱えるリクエスト数を増やせる設定はこれしかないからです。マルチスレッドのRubyアプリサーバーは、利用可能なCPUリソースやメモリリソースをよりうまく効果的に利用できるようになり、シングルスレッドのRubyアプリのプロセスと比較して1分間あたりに扱えるリクエスト数が多くなります。
現在のところ、Ruby webアプリをマルチスレッドで実行できる無料のアプリサーバーはPumaしかありません。Passenger Enterpriseならもちろんできますが、ライセンス料が必要になります。
NGINX UnitはPythonアプリ向けにマルチスレッドのサポートを計画中なので、今後いつの日にか、マルチスレッドのRubyアプリがサポートされる可能性はないとは言えません。
さて、現時点のNGINX UnitはUnicorn/Passenger/Pumaと比べてどれだけ革新的なのでしょうか?私の考えでは、伝統的なRailsアプリのセットアップ、つまり単一のモノリシックアプリ(モノリス)をHerokuなどのPaaS(Platform-as-a-Service)プロバイダ上で実行する場合は、現時点のNGINX Unitの機能や計画中のロードマップから得られるメリットはおそらくまったくないでしょう。Pumaは既にこうしたユーザー向けに非常にうまく機能しています。
NGINX Unitに関心を持つのは、おそらくリバースプロキシを廃止したがっているUnicornユーザーでしょう。NGINX UnitのHTTPS機能(訳注: 原文ではHTTPでした)が現実化すれば、UnicornとNGINXの組み合わせを単体のNGINX Unitサーバーに置き換えられるかもしれません。
おそらく、NGINX UnitはPhusion社のPassengerと直接比較されることが多くなるでしょう。Passengerもまた、Rubyと同様のJavaScriptやPythonのサポートによって最近「マイクロサービス」の分野に進出しました。NGINX Unitが現在サポートする言語数の方が多く、今後さらに増えると見込まれるので、おそらく自分の言語向けの手厚いサポートを求めるユーザーが移行することでしょう。しかしPhusion社はRubyを最優先する企業なので、Passengerでは常に、NGINX Unitがこれまで達成したサポートよりも完成度の高いサポートをRubyについて受けられることが期待できます。そして前述したように、Phusion Passenger Enterpriseはマルチスレッド実行を現にサポートしています。
では、理想のNGINX Unitアプリとは何でしょうか?独自のクラウドを運営していて(つまりHerokuのようにルーティングを代わりに管理してくれるクラウドではないということ)、Rubyバージョンの異なっているRubyアプリを多数動かしていたり、あるいはさまざまな言語で多数のサービスを動かしていてかつそれらのサービスが互いに高速でやりとりできる必要があるならば、NGINX Unitの設計はそうした用途に向いているように思えます。しかしこれに該当しないのであれば、従来の3強であるPuma/Passenger/Unicornを使い続けるのがおそらくベストでしょう。
関連記事
- これらのアプリサーバーの詳しい比較については、「Rails: Puma/Unicorn/Passengerの効率を最大化する設定」をご覧ください。 ↩
- ソフトウェアプロジェクトの宣伝文句で「モダンな」という言葉を見かけると心底うんざりします。そういうのは、何というかその分野の既存のソフトウェアプロジェクトを暗にdisっているようで、「あいつらはオワコン、俺たちは新しいぜっ」と言わんばかりです。しかも「古臭いもの」と比べて自分たちのどこがどう優れているのかを決してはっきり説明しようとしないのです。この手のマーケティングは、本質的な部分にはこれっぽっちも触れないばかりか、スキルセットが時代遅れになることを恐れているソフトウェア開発者たちの恐怖心につけこんで食い物にしています。 ↩