1. ホーム
  2. node.js

[解決済み] 一般的に、Node.jsは10,000の同時リクエストをどのように処理するのですか?

2022-03-17 05:07:18

質問

Node.jsはシングルスレッドとイベントループを使って、一度に1つずつしか処理しない(ノンブロッキングである)ことを理解しています。しかし、それでも、どのように動作するのでしょうか、例えば、10,000の同時リクエストを考えてみましょう。イベントループは、すべてのリクエストを処理するのでしょうか?それでは時間がかかりすぎるのではないでしょうか?

マルチスレッドのウェブサーバーより速いというのは、(まだ)理解できません。マルチスレッドのウェブサーバーはリソース(メモリ、CPU)が高くなることは理解していますが、それでも速くならないのでしょうか?おそらく私が間違っているのでしょう。このシングルスレッドがどのように多くのリクエストで速いのか、また10,000のような多くのリクエストを処理するときに通常何をするのか(高レベルで)説明してください。

また、そのシングルスレッドは、その大量でもうまくスケールするのでしょうか?私はNode.jsを学び始めたばかりであることを念頭に置いてください。

どのように解決するのですか?

もしこの質問をしなければならないとしたら、あなたはおそらくほとんどのウェブアプリケーションやサービスが何をするかについてよく知らないのでしょう。おそらく、すべてのソフトウェアがこれを行うと思っていることでしょう。

user do an action
       │
       v
 application start processing action
   └──> loop ...
          └──> busy processing
 end loop
   └──> send result to user

しかし、これはウェブアプリケーション、いや、データベースをバックエンドとするあらゆるアプリケーションの動作方法とは異なります。ウェブアプリケーションはこのように動作します。

user do an action
       │
       v
 application start processing action
   └──> make database request
          └──> do nothing until request completes
 request complete
   └──> send result to user

このシナリオでは、ソフトウェアの実行時間のほとんどを、データベースが戻るのを待つ0%のCPU時間として費やしています。

マルチスレッドなネットワークアプリ。

マルチスレッドネットワークアプリは、上記の作業負荷をこのように処理します。

request ──> spawn thread
              └──> wait for database request
                     └──> answer request
request ──> spawn thread
              └──> wait for database request
                     └──> answer request
request ──> spawn thread
              └──> wait for database request
                     └──> answer request

つまり、このスレッドは、データベースがデータを返すのを待つために、0%のCPUを使用してほとんどの時間を費やしています。その間に、スレッドごとに完全に独立したプログラムスタックなどを含むスレッドに必要なメモリを確保しなければなりません。また、スレッドを起動する必要がありますが、これはフルプロセスを起動するほど高価ではありませんが、それでも決して安くはないでしょう。

シングルスレッドイベントループ

私たちはほとんどの時間をCPUの0%を使って過ごしているので、CPUを使っていないときにいくつかのコードを実行してはどうでしょうか。そうすれば、各リクエストはマルチスレッド・アプリケーションと同じ量のCPU時間を得ることができますが、スレッドを開始する必要はありません。そこで、このようにします。

request ──> make database request
request ──> make database request
request ──> make database request
database request complete ──> send response
database request complete ──> send response
database request complete ──> send response

実際には、どちらのアプローチもほぼ同じレイテンシーでデータを返します。なぜなら、処理の大部分を占めるのはデータベースの応答時間だからです。

ここでの主な利点は、新しいスレッドを起動する必要がないため、たくさんのmallocを実行して速度を低下させる必要がないことです。

魔法のような、見えないスレッド

一見不思議に思えるのは、上記の2つのアプローチで、なぜワークロードを"parallel"で実行できるのか、ということです。その答えは、データベースがスレッド化されているからです。つまり、シングルスレッドのアプリは、実は別のプロセスであるデータベースのマルチスレッドな挙動を利用しているのです。

シングルスレッド・アプローチが失敗する場合

データを返す前に多くのCPU計算をする必要がある場合、シングルスレッドアプリは大きな失敗をします。ここで言う計算とは、データベースの結果を処理するforループのことではありません。これはまだほとんどO(n)です。つまり、フーリエ変換(例えばMP3エンコード)やレイトレーシング(3Dレンダリング)等のことです。

シングルスレッドアプリのもう1つの落とし穴は、1つのCPUコアしか利用しないことです。つまり、クアッドコアのサーバーを使用している場合(最近では珍しくありませんが)、他の3つのコアを使用しないことになります。

マルチスレッドアプローチが失敗する場合

マルチスレッド アプリは、スレッドごとに大量の RAM を割り当てる必要がある場合、大きな失敗をします。まず、RAM の使用量自体が、シングルスレッドアプリのように多くのリクエストを処理できないことを意味します。さらに悪いことに、mallocは遅いのです。最近のウェブフレームワークではよくあることですが、たくさんのオブジェクトを割り当てることは、シングルスレッドアプリよりも遅くなる可能性があることを意味します。これは、通常、node.jsが勝つところです。

マルチスレッドを悪化させてしまう使用例として、スレッド内で他のスクリプト言語を実行する必要がある場合があります。まず、その言語のランタイム全体をmallocする必要があり、次にスクリプトで使用する変数をmallocする必要があります。

ですから、もしあなたがCやgoやjavaでネットワークアプリケーションを書いているのなら、スレッドのオーバーヘッドは通常あまり気にならないはずです。もしあなたがPHPやRubyにサービスを提供するためにCのWebサーバーを書いているなら、javascriptやRubyやPythonでより速いサーバーを書くのはとても簡単です。

ハイブリッドアプローチ

ウェブサーバの中には、ハイブリッドなアプローチを採用しているものがあります。例えば Nginx と Apache2 は、ネットワーク処理コードをイベントループのスレッドプールとして実装しています。各スレッドは同時にイベントループを実行し、シングルスレッドでリクエストを処理しますが、リクエストは複数のスレッドに負荷分散されます。

シングルスレッド・アーキテクチャの中には、ハイブリッド・アプローチを採用しているものもあります。1つのプロセスから複数のスレッドを起動するのではなく、複数のアプリケーションを起動します。たとえば、クアッドコアマシンに4つのnode.jsサーバーを起動します。そして、ロードバランサーを使って、各プロセスの作業負荷を分散させるのです。

この2つのアプローチは、技術的には互いに同じ鏡像のようなものです。