1,000個のHTTPリクエストの並列トリガーがスタックする


10

問題は、1kから2kの発信HTTPリクエストをトリガーしたときに実際に何が起こっているかです。500接続ですべての接続を簡単に解決できることがわかりますが、そこから上に移動すると、接続が開いたままになり、ノードアプリがスタックしたままになるため、問題が発生するようです。ローカルサーバー+ Googleの例およびその他の模擬サーバーでテスト済み。

そのため、いくつかの異なるサーバーエンドポイントで理由を受け取りました。サーバーが要求を処理できず、エラーをスローして問題ないECONNRESETを読み取りました。1kから2kの要求範囲では、プログラムはハングします。開いている接続を確認すると、lsof -r 2 -i -aそこにぶら下がっている接続がX量あることがわかります0t0 TCP 192.168.0.20:54831->lk-in-f100.1e100.net:https (ESTABLISHED)。リクエストにタイムアウト設定を追加すると、タイムアウトエラーが発生する可能性がありますが、それ以外の場合は、接続が永久に維持され、メインプログラムがリンボ状態になります。

コード例:

import fetch from 'node-fetch';

(async () => {
  const promises = Array(1000).fill(1).map(async (_value, index) => {
    const url = 'https://google.com';
    const response = await fetch(url, {
      // timeout: 15e3,
      // headers: { Connection: 'keep-alive' }
    });
    if (response.statusText !== 'OK') {
      console.log('No ok received', index);
    }
    return response;
  })

  try {
    await Promise.all(promises);
  } catch (e) {
    console.error(e);
  }
  console.log('Done');
})();

1
でしたが、あなたのポスト結果npx envinfo、8432.805msで私の勝利10 / nodev10.16.0スクリプトの端にあなたの例を実行している
ルカシュSzewczak

OS XとAlpine Linux(dockerコンテナー)で例を実行し、同じ結果に達しました。
Risto Novik

私のローカルMacは7156.797msでスクリプトを実行します。リクエストをブロックしているファイアウォールがないことを確認しますか?
John

ローカルマシンのファイアウォールを使用せずにテストされましたが、ローカルルーター/ネットワークに問題がある可能性がありますか?Google CloudまたはHerokuで同様のテストを実行してみます。
Risto Novik

回答:


3

確かに何が起こっているのかを理解するために、スクリプトにいくつかの変更を加える必要がありましたが、ここにあります。

最初に、あなたはnodeその方法とそのevent loop機能を知っているかもしれませんが、簡単に要約します。スクリプトを実行すると、nodeランタイムは最初にその同期部分を実行し、次にpromisestimersをスケジュールして次のループで実行するようにスケジュールします。スクリプトが解決されると、別のループでコールバックを実行します。この単純な要旨はそれを非常によく説明しています。@ StephenGriderの功績です。


const pendingTimers = [];
const pendingOSTasks = [];
const pendingOperations = [];

// New timers, tasks, operations are recorded from myFile running
myFile.runContents();

function shouldContinue() {
  // Check one: Any pending setTimeout, setInterval, setImmediate?
  // Check two: Any pending OS tasks? (Like server listening to port)
  // Check three: Any pending long running operations? (Like fs module)
  return (
    pendingTimers.length || pendingOSTasks.length || pendingOperations.length
  );
}

// Entire body executes in one 'tick'
while (shouldContinue()) {
  // 1) Node looks at pendingTimers and sees if any functions
  // are ready to be called.  setTimeout, setInterval
  // 2) Node looks at pendingOSTasks and pendingOperations
  // and calls relevant callbacks
  // 3) Pause execution. Continue when...
  //  - a new pendingOSTask is done
  //  - a new pendingOperation is done
  //  - a timer is about to complete
  // 4) Look at pendingTimers. Call any setImmediate
  // 5) Handle any 'close' events
}

// exit back to terminal

保留中のOSタスクがあるまで、イベントループは決して終了しないことに注意してください。つまり、保留中のHTTPリクエストがあるまで、ノードの実行は終了しません。

あなたのケースでは、async常にpromiseを返すため、関数を実行し、次のループ反復で実行されるようにスケジュールします。非同期関数で、その反復で一度に別の1000件のプロミス(HTTPリクエスト)をスケジュールしmapます。その後、すべてが解決されてプログラムが完了するのを待っています。上の無名矢印関数がエラーをmapスローしない限り、確実に機能します。あなたの約束の一つがエラーをスローし、あなたがそれを処理しない場合は、約束のいくつかは、彼らのコールバックがにプログラムを作る、これまでと呼ばれる必要はありません終わりではなくに終了イベントループが解決されるまでに終了するには、それを防ぐことができますので、コールバックなしでも、すべてのタスク。それが言うようにPromise.all docs:最初のプロミスが拒否されるとすぐに拒否されます。

したがって、あなたのECONNRESETエラーはノード自体に関連しておらず、ネットワークでフェッチを行ってエラーをスローし、イベントループが終了するのを防ぎました。この小さな修正により、非同期で解決されるすべてのリクエストを確認できます。

const fetch = require("node-fetch");

(async () => {
  try {
    const promises = Array(1000)
      .fill(1)
      .map(async (_value, index) => {
        try {
          const url = "https://google.com/";
          const response = await fetch(url);
          console.log(index, response.statusText);
          return response;
        } catch (e) {
          console.error(index, e.message);
        }
      });
    await Promise.all(promises);
  } catch (e) {
    console.error(e);
  } finally {
    console.log("Done");
  }
})();

ねえ、ペドロ、説明してくれてありがとう。最初のプロミス拒否が表示されたときにPromise.allが拒否されることは承知していますが、ほとんどの場合、拒否するエラーはなく、すべてがアイドル状態になります。
Risto Novik

1
>保留中のOSタスクが存在するまでイベントループが終了しないように修復します。つまり、保留中のHTTPリクエストがあるまで、ノードの実行は終了しません。これは興味深い点のようです、OSタスクはlibuvを通じて管理されますか?
Risto Novik

libuvは操作に関連するより多くのもの(マルチスレッドを本当に必要とするもの)を処理すると思います。しかし、私は間違っている可能性があります。詳細を確認する必要があります
ペドロムター
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.