การเรียกขนานคำขอ HTTP 1k จะติด


10

คำถามคือสิ่งที่เกิดขึ้นจริงเมื่อคุณเรียกใช้คำขอ HTTP ขาออก 1k-2k ฉันเห็นว่ามันจะแก้ปัญหาการเชื่อมต่อทั้งหมดได้อย่างง่ายดายด้วยการเชื่อมต่อ 500 ครั้ง แต่การขยับขึ้นจากที่นั่นดูเหมือนจะทำให้เกิดปัญหาเนื่องจากการเชื่อมต่อถูกเปิดทิ้งไว้และแอป Node จะติดอยู่ที่นั่น ทดสอบกับเซิร์ฟเวอร์ในพื้นที่ + ตัวอย่าง 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ใช้งานตัวอย่างของคุณในสคริปต์ Win 10 / nodev10.16.0 ของฉันสิ้นสุดใน 8432.805ms
Łukasz Szewczak

ฉันเรียกใช้ตัวอย่างบน OS X และ Alpine Linux (docker container) และถึงผลลัพธ์เดียวกัน
Risto Novik

แม็คในเครื่องของฉันรันสคริปต์ใน 7156.797ms คุณแน่ใจหรือว่าไม่มีไฟร์วอลล์ปิดกั้นการร้องขอ
จอห์

ทดสอบโดยไม่ใช้ไฟร์วอลล์ในเครื่อง แต่อาจเป็นปัญหากับเราเตอร์ / เครือข่ายท้องถิ่นของฉันได้หรือไม่ ฉันจะพยายามทำการทดสอบที่คล้ายกันใน Google Cloud หรือ Heroku
Risto Novik

คำตอบ:


3

เพื่อให้เข้าใจว่าเกิดอะไรขึ้นฉันต้องแก้ไขสคริปต์ของคุณ แต่ที่นี่มี

ก่อนอื่นคุณอาจรู้วิธีnodeและevent loopผลงานของมันแต่ขอให้ฉันทำการสรุปอย่างรวดเร็ว เมื่อคุณเรียกใช้สคริปต์nodeรันไทม์ก่อนจะรันส่วนซิงโครนัสของมันจากนั้นกำหนดเวลาpromisesและtimersที่จะดำเนินการในลูปถัดไปและเมื่อตรวจสอบพวกเขาจะได้รับการแก้ไขเรียกใช้การเรียกกลับในวงอื่น ส่วนสำคัญที่เรียบง่ายนี้อธิบายได้ดีมากให้เครดิตกับ @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ฟังก์ชั่นเนื่องจากมันจะคืนสัญญาเสมอมันจะกำหนดให้มันถูกเรียกใช้ในการวนซ้ำครั้งถัดไป ในฟังก์ชั่น async ของคุณคุณกำหนดอีก1,000สัญญา (คำขอ HTTP) ในครั้งเดียวในmapการทำซ้ำนั้น หลังจากนั้นคุณกำลังรอการแก้ไขทั้งหมดเพื่อจบโปรแกรม มันจะทำงานเพื่อตรวจสอบว่าเว้นแต่ฟังก์ชั่นที่ไม่ระบุชื่อลูกศรของคุณบนmapไม่เคยโยนใด ๆ ที่ผิดพลาด หากสัญญาข้อใดข้อหนึ่งของคุณเกิดข้อผิดพลาดและคุณไม่ได้จัดการมันสัญญาบางรายการจะไม่มีการเรียกกลับที่ทำให้โปรแกรมสิ้นสุดแต่ไม่ออกเนื่องจากห่วงเหตุการณ์จะป้องกันไม่ให้ออกจนกว่าจะแก้ไขได้ งานทั้งหมดแม้ไม่มีการติดต่อกลับ ตามที่ระบุไว้ในPromise.all เอกสาร : มันจะปฏิเสธทันทีที่สัญญาแรกปฏิเสธ

ดังนั้น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 ที่รอการอนุมัติ ดูเหมือนว่าเป็นจุดที่น่าสนใจงานระบบปฏิบัติการได้รับการจัดการผ่าน libuv หรือไม่
Risto Novik

ฉันเดาว่า libuv จัดการสิ่งต่าง ๆ ที่เกี่ยวข้องกับการปฏิบัติการได้มากขึ้น (สิ่งที่ต้องใช้มัลติเธรดจริงๆ) แต่ฉันอาจจะผิดต้องดูเพิ่มเติมในเชิงลึก
Pedro Mutter
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.