Memicu paralel permintaan HTTP 1k akan macet

10

Pertanyaannya adalah apa yang sebenarnya terjadi ketika Anda memicu permintaan HTTP keluar 1k-2k? Saya melihat bahwa itu akan menyelesaikan semua koneksi dengan mudah dengan 500 koneksi tetapi bergerak ke atas dari sana tampaknya menyebabkan masalah karena koneksi dibiarkan terbuka dan aplikasi Node akan macet di sana. Diuji dengan server lokal + contoh Google dan server tiruan lainnya.

Jadi dengan beberapa titik akhir server yang berbeda saya memang menerima alasan: baca ECONNRESET yang baik-baik saja server tidak dapat menangani permintaan dan melemparkan kesalahan. Dalam rentang permintaan 1k-2k program hanya akan hang. Ketika Anda memeriksa koneksi terbuka dengan lsof -r 2 -i -aAnda bisa melihat bahwa ada beberapa jumlah koneksi X yang terus menggantung di sana 0t0 TCP 192.168.0.20:54831->lk-in-f100.1e100.net:https (ESTABLISHED). Ketika Anda menambahkan pengaturan batas waktu ke permintaan, ini mungkin akan berakhir dengan kesalahan batas waktu, tetapi mengapa jika koneksi tidak terputus selamanya dan program utama akan berakhir dalam keadaan limbo?

Kode contoh:

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');
})();
Risto Novik
sumber
1
Bisakah Anda memposting hasil npx envinfo, menjalankan contoh Anda pada skrip Win 10 / nodev10.16.0 saya berakhir di 8432.805ms
Łukasz Szewczak
Saya menjalankan contoh di OS X dan Alpine Linux (buruh pelabuhan) dan mencapai hasil yang sama.
Risto Novik
Mac lokal saya menjalankan skrip dalam 7156.797 ms. Apakah Anda yakin tidak ada Firewall yang memblokir permintaan?
John
Diuji tanpa menggunakan firewall mesin lokal, tetapi bisakah itu menjadi masalah dengan router / jaringan lokal saya? Saya akan mencoba menjalankan tes serupa di Google Cloud atau Heroku.
Risto Novik

Jawaban:

3

Untuk memahami apa yang terjadi dengan pasti, saya perlu membuat beberapa modifikasi pada skrip Anda, tetapi di sini ada.

Pertama, Anda mungkin tahu cara nodedan event loopkerjanya, tetapi izinkan saya membuat rekap cepat. Saat Anda menjalankan skrip, noderuntime pertama kali menjalankan bagian sinkronnya kemudian menjadwalkan promisesdan timersuntuk dieksekusi pada loop berikutnya, dan ketika diperiksa mereka diselesaikan, jalankan callback di loop lain. Intisari sederhana ini menjelaskan dengan sangat baik, kredit ke @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

Perhatikan bahwa loop acara tidak akan pernah berakhir sampai ada tugas OS yang tertunda. Dengan kata lain, eksekusi node Anda tidak akan pernah berakhir sampai ada permintaan HTTP yang tertunda.

Dalam kasus Anda, ini menjalankan asyncfungsi, karena ia akan selalu mengembalikan janji, itu akan menjadwalkannya untuk dieksekusi dalam iterasi loop berikutnya. Pada fungsi async Anda, Anda menjadwalkan 1000 janji lain (permintaan HTTP) sekaligus dalam mapiterasi itu. Setelah itu, Anda menunggu semuanya diselesaikan untuk menyelesaikan program. Ini akan bekerja, pasti, kecuali fungsi panah anonim Anda pada maptidak membuang kesalahan . Jika salah satu janji Anda membuat kesalahan dan Anda tidak menanganinya, beberapa janji tidak akan membuat panggilan baliknya pernah membuat program untuk mengakhiri tetapi tidak untuk keluar , karena loop acara akan mencegahnya untuk keluar sampai diselesaikan semua tugas, bahkan tanpa panggilan balik. Seperti yang tertulis diPromise.all docs : itu akan menolak segera setelah janji pertama ditolak.

Jadi, ECONNRESETkesalahan Anda tidak terkait dengan node itu sendiri, adalah sesuatu dengan jaringan Anda yang membuat pengambilan untuk melempar kesalahan dan kemudian mencegah event loop berakhir. Dengan perbaikan kecil ini, Anda dapat melihat semua permintaan diselesaikan secara tidak sinkron:

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");
  }
})();
Pedro Mutter
sumber
Hei, Pedro terima kasih atas upaya menjelaskannya. Saya sadar bahwa Janji itu. Semua akan menolak ketika penolakan janji pertama muncul tetapi dalam kebanyakan kasus tidak ada kesalahan untuk menolak sehingga semuanya hanya akan menganggur.
Risto Novik
1
> Memperbaiki bahwa loop acara tidak akan pernah berakhir sampai ada tugas OS yang tertunda. Dengan kata lain, eksekusi node Anda tidak akan pernah berakhir sampai ada permintaan HTTP yang tertunda. Ini sepertinya poin yang menarik, tugas OS dikelola melalui libuv?
Risto Novik
Saya kira libuv menangani lebih banyak hal yang berhubungan dengan operasi (hal-hal yang benar-benar membutuhkan multi-threading). Tapi saya bisa saja salah, perlu melihat lebih dalam
Pedro Mutter