ホームページ  >  記事  >  バックエンド開発  >  Node、PHP、Java、Go サーバーの I/O パフォーマンス競争、誰が勝つと思いますか?

Node、PHP、Java、Go サーバーの I/O パフォーマンス競争、誰が勝つと思いますか?

醉折花枝作酒筹
醉折花枝作酒筹転載
2021-07-22 09:26:333506ブラウズ

この記事では、まず I/O に関連する基本概念を簡単に紹介し、次に Node、PHP、Java、Go の I/O パフォーマンスを水平的に比較し、選択のヒントを示します。以下に紹介しますので、必要な友達は参考にしてください。

Node、PHP、Java、Go サーバーの I/O パフォーマンス競争、誰が勝つと思いますか?

アプリケーションの入出力 (I/O) モデルを理解すると、理想的かつ実際にアプリケーションが負荷を処理する方法をよりよく理解できます。おそらくアプリケーションは小さく、高負荷をサポートする必要がないため、考慮すべき点が少なくなります。ただし、アプリケーションのトラフィック負荷が増加すると、間違った I/O モデルを使用すると、非常に深刻な結果が生じる可能性があります。

この記事では、Node、Java、Go、PHP と Apache を比較し、さまざまな言語が I/O をモデル化する方法、各モデルの長所と短所、および基本的なパフォーマンスのレビューについて説明します。次の Web アプリケーションの I/O パフォーマンスが気になる場合は、この記事が役に立ちます。

I/O の基本: 簡単な復習

I/O に関連する要素を理解するには、まずオペレーティング システム レベルでこれらの概念を理解する必要があります。最初に多くの概念に直接触れることはほとんどありませんが、アプリケーションの操作中に直接的または間接的に常にそれらの概念に遭遇することになります。詳細が重要です。

システム コール

まず、システム コールについて説明します。具体的には次のとおりです:

  • アプリケーションがオペレーティング システム カーネルに要求します。それに対する I/O を実行する O 操作。

  • 「システムコール」とは、プログラムがカーネルに特定の操作の実行を要求することです。実装の詳細はオペレーティング システムによって異なりますが、基本的な概念は同じです。 「システムコール」が実行されると、プログラムを制御するための特定の命令がカーネルに転送されます。一般に、システム コールはブロッキングです。これは、カーネルが結果を返すまでプログラムが待機することを意味します。

  • カーネルは、物理デバイス (ディスク、ネットワーク カードなど) 上で低レベルの I/O 操作を実行し、システム コールに応答します。現実の世界では、カーネルはリクエストを満たすために、デバイスの準備ができるのを待つ、内部状態を更新するなど、多くのことを行う必要があるかもしれませんが、アプリケーション開発者としては気にする必要はありません。それについては、カーネルの仕事です。

ブロック呼び出しと非ブロック呼び出し

システム コールは一般的にブロックであると上で述べました。ただし、一部の呼び出しは「ノンブロッキング」です。これは、カーネルがリクエストをキューまたはバッファーに入れ、実際の I/O が発生するのを待たずにすぐに戻ります。したがって、「ブロック」されるのは短時間だけですが、キューには一定の時間がかかります。

この点を説明するために、いくつかの例 (Linux システム コール) を示します。

  • read() はブロッキング コールです。データを保存するためにファイル ハンドルとバッファを渡し、データがバッファに保存されたときに返す必要があります。シンプルでありながら高級感があるのが特徴です。

  • epoll_create()、epoll_ctl()、および epoll_wait() を使用すると、監視するハンドルのグループを作成し、このグループ内のハンドルを追加/削除し、ハンドルが見つかるまでプログラムをブロックできます。ハンドル上のあらゆるアクティビティ。これらのシステム コールを使用すると、単一のスレッドのみを使用して多数の I/O 操作を効率的に制御できます。これらの機能は非常に便利ですが、使用するのは非常に複雑です。

ここでの時差の大きさを理解することが重要です。最適化されていない CPU コアが 3GHz で実行される場合、1 秒あたり 30 億サイクル (つまり、1 ナノ秒あたり 3 サイクル) 実行できます。ノンブロッキング システム コールには 10 サイクル以上、つまり数ナノ秒かかる場合があります。ネットワークから情報を受信するための呼び出しをブロックすると、さらに時間がかかる場合があります (たとえば 200 ミリ秒 (1/5 秒))。

たとえば、ノンブロッキング呼び出しには 20 ナノ秒かかり、ブロッキング呼び出しには 200,000,000 ナノ秒かかりました。このように、プロセスは呼び出しをブロックするために 1,000 万サイクル待機する必要がある場合があります。

カーネルは、ブロッキング I/O (「ネットワークからデータを読み取る」) とノンブロッキング I/O (「ネットワーク接続上に新しいデータがあるときに通知する」) の 2 つのメソッドを提供します。メカニズムが呼び出しプロセスをブロックする時間の長さはまったく異なります。

スケジューリング

3 番目の非常に重要なことは、多くのスレッドまたはプロセスがブロックされ始めたときに何が起こるかです。

私たちにとって、スレッドとプロセスの間に大きな違いはありません。実際には、パフォーマンスに関する最も大きな違いは、スレッドが同じメモリを共有し、各プロセスが独自のメモリ空間を持っているため、単一プロセスがより多くのメモリを占有する傾向があることです。ただし、スケジューリングについて話すときは、実際には一連の作業を完了することについて話しており、それぞれの作業には、利用可能な CPU コアで一定量の実行時間が必要です。

8 コアで 300 スレッドを実行している場合は、各スレッドがタイム スライスを取得し、各コアが短時間実行されてから次のスレッドに切り替わるように時間をスライスする必要があります。これは、CPU があるスレッド/プロセスから次のスレッド/プロセスに切り替えることができる「コンテキスト スイッチ」によって行われます。

この種のコンテキスト切り替えには一定のコストがかかります。つまり、一定の時間がかかります。高速な場合は 100 ナノ秒未満の場合もありますが、実装の詳細、プロセッサの速度/アーキテクチャ、CPU キャッシュ、その他のソフトウェアとハ​​ードウェアが異なる場合は、1000 ナノ秒以上かかるのが通常です。

スレッド (またはプロセス) の数が増えると、コンテキスト スイッチの数も増えます。スレッドが数千あり、各スレッドの切り替えに数百ナノ秒かかる場合、システムは非常に遅くなります。

ただし、ノンブロッキング呼び出しは基本的にカーネルに「これらの接続に新しいデータまたはイベントが到着した場合にのみ呼び出してください」と指示します。これらのノンブロッキング呼び出しは、大きな I/O 負荷を効率的に処理し、コンテキストの切り替えを減らします。

この記事の例は小さいですが、データベース アクセス、外部キャッシュ システム (memcache など)、および I/O を必要とするものはすべて、最終的に何らかのタイプの I/O 呼び出しを実行することに注意してください。 、これは例と同じ原理です。

プロジェクトにおけるプログラミング言語の選択には、パフォーマンスだけを考慮したとしても、多くの要因が影響します。ただし、プログラムが主に I/O によって制限されており、パフォーマンスがプロジェクトの成功または失敗を決定する重要な要素であることが心配な場合は、次の提案を考慮する必要があります。

「Keep It Simple」: PHP

1990 年代には、コンバースの靴を履いて Perl で CGI スクリプトを書いている人がたくさんいました。その後、PHP が登場し、多くの人に気に入られ、動的な Web ページを簡単に作成できるようになりました。

PHP で使用されるモデルは非常に単純です。まったく同じにすることは不可能ですが、一般的な PHP サーバーの原則は次のとおりです。

ユーザーのブラウザが HTTP リクエストを発行し、リクエストは Apache Web サーバーに入ります。 Apache はリクエストごとに個別のプロセスを作成し、いくつかの最適化メソッドを通じてこれらのプロセスを再利用して、実行する必要がある操作を最小限に抑えます (プロセスの作成には比較的時間がかかります)。

Apache は PHP を呼び出し、ディスク上の特定の .php ファイルを実行するように指示します。

PHP コードの実行が開始され、I/O 呼び出しがブロックされます。 PHP で呼び出す file_get_contents() は、実際には read() システム コールを呼び出し、返される結果を待ちます。

<?php// blocking file I/O$file_data = file_get_contents(‘/path/to/file.dat’);

// blocking network I/O$curl = curl_init(&#39;http://example.com/example-microservice&#39;);
$result = curl_exec($curl);

// some more blocking network I/O$result = $db->query(&#39;SELECT id, data FROM examples ORDER BY id DESC limit 100&#39;);

?>

それは簡単です。リクエストごとに 1 つのプロセスです。 I/O 呼び出しがブロックされています。メリットについてはどうでしょうか?シンプルですが効果的です。デメリットについてはどうでしょうか?同時クライアントが 20,000 ある場合、サーバーは麻痺します。このアプローチは、大量の I/O を処理するためにカーネルによって提供されるツール (epoll など) が十分に活用されていないため、拡張することが困難です。さらに悪いことに、リクエストごとに個別のプロセスを実行すると、多くのシステム リソース、特にメモリが消費される傾向があり、メモリが最初に使い果たされることがよくあります。

#**注: この時点で、Ruby の状況は PHP の状況と非常に似ています。

マルチスレッド: Java

そこで、Java が登場しました。また、Java には言語にマルチスレッドが組み込まれており、特にスレッドの作成に関しては優れています。

ほとんどの Java Web サーバーは、リクエストごとに新しい実行スレッドを開始し、このスレッドで開発者が作成した関数を呼び出します。

Java サーブレットでの I/O の実行は、次のようになります:

publicvoiddoGet(HttpServletRequest request,
    HttpServletResponse response) throws ServletException, IOException
{

    // blocking file I/O
    InputStream fileIs = new FileInputStream("/path/to/file");

    // blocking network I/O
    URLConnection urlConnection = (new URL("http://example.com/example-microservice")).openConnection();
    InputStream netIs = urlConnection.getInputStream();

    // some more blocking network I/O
out.println("...");
}

上記の doGet メソッドはリクエストに対応し、独立したメソッドを必要とするのではなく、独自のスレッドで実行されるため、別個のスレッドで実行されます。メモリのプロセスに問題があるため、別のスレッドを作成します。各リクエストは新しいスレッドを取得し、リクエストが処理されるまでさまざまな I/O 操作がそのスレッド内でブロックされます。アプリケーションはスレッドの作成と破棄のコストを最小限に抑えるためにスレッド プールを作成しますが、数千の接続は数千のスレッドを意味するため、スケジューラにとっては良いことではありません。

Java バージョン 1.4 (バージョン 1.7 で再アップグレード) では、ノンブロッキング I/O を呼び出す機能が追加されていることに注目してください。ほとんどのアプリケーションはこの機能を使用しませんが、少なくとも利用可能です。一部の Java Web サーバーはこの機能を実験していますが、デプロイされた Java アプリケーションの大部分は依然として上記の原則に従って動作します。

Java は、すぐに使用できる I/O 機能を多数提供しますが、多数の I/O 操作を実行するために多数のブロック スレッドを作成する状況が発生した場合、Java は、良い解決策があります。

ノンブロッキング I/O を最優先事項にします: Node

I/O のパフォーマンスが高く、ユーザーの間で人気があるのは Node.js です。 Node の基本を理解している人なら誰でも、Node が「ノンブロッキング」であり、I/O を効率的に処理できることを知っています。これは一般的な意味で真実です。ただし、詳細と実装方法が重要です。

I/O を伴う操作を行う必要がある場合は、リクエストを作成してコールバック関数を与える必要があり、ノードはリクエストの処理後にこの関数を呼び出します。

リクエストで I/O 操作を実行する一般的なコードは次のとおりです。

http.createServer(function(request, response) {
    fs.readFile(&#39;/path/to/file&#39;, &#39;utf8&#39;, function(err, data) {
        response.end(data);
    });
});

上に示したように、コールバック関数が 2 つあります。最初の関数はリクエストの開始時に呼び出され、2 番目の関数はファイル データが利用可能になったときに呼び出されます。

这样,Node就能更有效地处理这些回调函数的I/O。有一个更能说明问题的例子:在Node中调用数据库操作。首先,你的程序开始调用数据库操作,并给Node一个回调函数,Node会使用非阻塞调用来单独执行I/O操作,然后在请求的数据可用时调用你的回调函数。这种对I/O调用进行排队并让Node处理I/O调用然后得到一个回调的机制称为“事件循环”。这个机制非常不错。

然而,这个模型有一个问题。在底层,这个问题出现的原因跟V8 JavaScript引擎(Node使用的是Chrome的JS引擎)的实现有关,即:你写的JS代码都运行在一个线程中。请思考一下。这意味着,尽管使用高效的非阻塞技术来执行I/O,但是JS代码在单个线程操作中运行基于CPU的操作,每个代码块都会阻塞下一个代码块的运行。有一个常见的例子:在数据库记录上循环,以某种方式处理记录,然后将它们输出到客户端。下面这段代码展示了这个例子的原理:

var handler = function(request, response) {

    connection.query(&#39;SELECT ...&#39;, function(err, rows) {if (err) { throw err };

        for (var i = 0; i < rows.length; i++) {
            // do processing on each row
        }

        response.end(...); // write out the results

    })

};

虽然Node处理I/O的效率很高,但是上面例子中的for循环在一个主线程中使用了CPU周期。这意味着如果你有10000个连接,那么这个循环就可能会占用整个应用程序的时间。每个请求都必须要在主线程中占用一小段时间。

这整个概念的前提是I/O操作是最慢的部分,因此,即使串行处理是不得已的,但对它们进行有效处理也是非常重要的。这在某些情况下是成立的,但并非一成不变。

另一点观点是,写一堆嵌套的回调很麻烦,有些人认为这样的代码很丑陋。在Node代码中嵌入四个、五个甚至更多层的回调并不罕见。

又到了权衡利弊的时候了。如果你的主要性能问题是I/O的话,那么这个Node模型能帮到你。但是,它的缺点在于,如果你在一个处理HTTP请求的函数中放入了CPU处理密集型代码的话,一不小心就会让每个连接都出现拥堵。

原生无阻塞:Go

在介绍Go之前,我透露一下,我是一个Go的粉丝。我已经在许多项目中使用了Go。

让我们看看它是如何处理I/O的吧。 Go语言的一个关键特性是它包含了自己的调度器。它并不会为每个执行线程对应一个操作系统线程,而是使用了“goroutines”这个概念。Go运行时会为一个goroutine分配一个操作系统线程,并控制它执行或暂停。Go HTTP服务器的每个请求都在一个单独的Goroutine中进行处理。

实际上,除了回调机制被内置到I/O调用的实现中并自动与调度器交互之外,Go运行时正在做的事情与Node不同。它也不会受到必须让所有的处理代码在同一个线程中运行的限制,Go会根据其调度程序中的逻辑自动将你的Goroutine映射到它认为合适的操作系统线程中。因此,它的代码是这样的:

func ServeHTTP(w http.ResponseWriter, r *http.Request) {

    // the underlying network call here is non-blocking
    rows, err := db.Query("SELECT ...")

    for _, row := range rows {
        // do something with the rows,// each request in its own goroutine
    }

    w.Write(...) // write the response, also non-blocking

}

如上所示,这样的基本代码结构更为简单,而且还实现了非阻塞I/O。

在大多数情况下,这真正做到了“两全其美”。非阻塞I/O可用于所有重要的事情,但是代码却看起来像是阻塞的,因此这样往往更容易理解和维护。 剩下的就是Go调度程序和OS调度程序之间的交互处理了。这并不是魔法,如果你正在建立一个大型系统,那么还是值得花时间去了解它的工作原理的。同时,“开箱即用”的特点使它能够更好地工作和扩展。

Go可能也有不少缺点,但总的来说,它处理I/O的方式并没有明显的缺点。

性能评测

对于这些不同模型的上下文切换,很难进行准确的计时。当然,我也可以说这对你并没有多大的用处。这里,我将对这些服务器环境下的HTTP服务进行基本的性能评测比较。请记住,端到端的HTTP请求/响应性能涉及到的因素有很多。

我针对每一个环境都写了一段代码来读取64k文件中的随机字节,然后对其运行N次SHA-256散列(在URL的查询字符串中指定N,例如.../test.php?n=100)并以十六进制打印结果。我之所以选择这个,是因为它可以很容易运行一些持续的I/O操作,并且可以通过受控的方式来增加CPU使用率。

在这种存在大量连接和计算的情况下,我们看到的结果更多的是与语言本身的执行有关。请注意,“脚本语言”的执行速度最慢。

各リクエスト内の CPU を集中的に使用する操作が相互にブロックされるため、突然、ノードのパフォーマンスが大幅に低下します。興味深いことに、このテストでは、PHP のパフォーマンスが (他のテストと比較して) 向上し、Java よりもさらに向上しました。 (PHP では SHA-256 の実装が C で記述されていることに注意してください。ただし、今回は 1000 回のハッシュ反復を行うため、このループでは実行パスに時間がかかります)。

接続数が増えると、PHP Apache の新しいプロセスとメモリのアプリケーションが PHP のパフォーマンスに影響を与える主な要因になると思われます。明らかに、今回は Go が勝者で、次に Java、Node、そして最後に PHP が続きます。

全体的なスループットには多くの要因が関係しており、それらはアプリケーションごとに大きく異なりますが、基礎となる原理と関係するトレードオフを理解すればするほど、より理解が深まります。パフォーマンスが向上します。

まとめ

要約すると、言語が進化するにつれて、大量の I/O を処理する大規模なアプリケーション向けのソリューションも進化します。

公平を期すために言うと、PHP と Java の両方には、Web アプリケーション用のノンブロッキング I/O 実装が用意されています。ただし、これらの実装は上記の方法ほど広く使用されておらず、メンテナンスのオーバーヘッドを考慮する必要があります。言うまでもなく、アプリケーションのコードはこの環境に適した方法で構造化されている必要があります。

パフォーマンスと使いやすさに影響を与えるいくつかの重要な要素を比較してみましょう:

言語 スレッドとプロセス ノンブロッキング I/O 使いやすい
PHP プロセス No -
Java スレッド 有効 コールバックが必要
Node.js Thread is コールバックが必要
Go Thread(Goroutines ) はい コールバックは必要ありません

スレッドは同じメモリ空間を共有しますが、プロセスは共有しないため、通常はスレッドの方が大きくなりますこのプロセスはメモリ効率がはるかに優れています。上記のリストでは、上から下に見ると、I/O 関連の要因が最後のものよりも優れています。したがって、上記の比較で勝者を選ばなければならないとしたら、間違いなく Go でしょう。

とはいえ、実際には、アプリケーションを構築する環境の選択は、チームが環境に精通していることと、チームが達成できる全体的な生産性と密接に関係しています。したがって、Node または Go を使用して Web アプリケーションやサービスを開発することは、チームにとって最良の選択ではない可能性があります。

これが、内部で何が起こっているのかをより明確に理解し、アプリケーションのスケーラビリティを処理する方法についていくつかの提案が得られることを願っています。

推奨学習: php ビデオ チュートリアル

以上がNode、PHP、Java、Go サーバーの I/O パフォーマンス競争、誰が勝つと思いますか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcsdn.netで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。