Kích hoạt song song các yêu cầu HTTP 1k sẽ bị kẹt


10

Câu hỏi đặt ra là điều gì đang thực sự xảy ra khi bạn kích hoạt các yêu cầu HTTP gửi đi 1k-2k? Tôi thấy rằng nó sẽ giải quyết tất cả các kết nối dễ dàng với 500 kết nối nhưng di chuyển lên từ đó dường như gây ra sự cố vì các kết nối bị bỏ ngỏ và ứng dụng Node sẽ bị kẹt ở đó. Đã thử nghiệm với máy chủ cục bộ + ví dụ Google và các máy chủ giả khác.

Vì vậy, với một số điểm cuối máy chủ khác nhau, tôi đã nhận được lý do: đọc ECONNRESET, điều đó tốt, máy chủ không thể xử lý yêu cầu và đưa ra lỗi. Trong phạm vi yêu cầu 1k-2k, chương trình sẽ bị treo. Khi bạn kiểm tra các kết nối mở với lsof -r 2 -i -abạn có thể thấy rằng có một số lượng X kết nối liên tục bị treo ở đó 0t0 TCP 192.168.0.20:54831->lk-in-f100.1e100.net:https (ESTABLISHED). Khi bạn thêm cài đặt thời gian chờ vào các yêu cầu, những yêu cầu này có thể sẽ bị lỗi hết thời gian, nhưng tại sao kết nối lại được duy trì mãi mãi và chương trình chính sẽ kết thúc ở trạng thái limbo?

Mã ví dụ:

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
Bạn có thể đăng kết quả của npx envinfo, chạy ví dụ của bạn trên tập lệnh Win 10 / gậtv10.16.0 của tôi kết thúc sau 8432.805ms
ukasz Szewczak

Tôi chạy ví dụ trên OS X và Alpine Linux (container docker) và đạt được kết quả tương tự.
Risto Novik

Mac cục bộ của tôi chạy tập lệnh trong 7156.797ms. Bạn có chắc chắn không có Tường lửa chặn các yêu cầu?
Giăng

Đã kiểm tra mà không sử dụng tường lửa máy cục bộ, nhưng nó có thể là một vấn đề với bộ định tuyến / mạng cục bộ của tôi không? Tôi sẽ thử chạy thử nghiệm tương tự trong Google Cloud hoặc Heroku.
Risto Novik

Câu trả lời:


3

Để hiểu những gì đang xảy ra chắc chắn, tôi cần phải thực hiện một số sửa đổi cho kịch bản của bạn, nhưng ở đây có.

Đầu tiên, bạn có thể biết cách thức nodeevent loophoạt động của nó , nhưng hãy để tôi tóm tắt nhanh. Khi bạn chạy một tập lệnh, nodetrước tiên , thời gian chạy sẽ chạy phần đồng bộ của tập lệnh sau đó lên lịch promisestimersđể được thực thi trên các vòng lặp tiếp theo và khi được kiểm tra chúng được giải quyết, hãy chạy các cuộc gọi lại trong một vòng lặp khác. Ý chính đơn giản này giải thích nó rất tốt, tín dụng cho @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

Lưu ý rằng vòng lặp sự kiện sẽ không bao giờ kết thúc cho đến khi có các tác vụ HĐH đang chờ xử lý. Nói cách khác, việc thực thi nút của bạn sẽ không bao giờ kết thúc cho đến khi có các yêu cầu HTTP đang chờ xử lý.

Trong trường hợp của bạn, nó chạy một asynchàm, vì nó sẽ luôn trả lại một lời hứa, nó sẽ lên lịch để nó được thực hiện trong vòng lặp tiếp theo. Trên chức năng không đồng bộ của bạn, bạn lên lịch 1000 lời hứa (yêu cầu HTTP) cùng một lúc trong maplần lặp đó . Sau đó, bạn đang chờ tất cả sau đó được giải quyết để kết thúc chương trình. Nó sẽ hoạt động, chắc chắn, trừ khi chức năng mũi tên ẩn danh của bạn mapkhông ném bất kỳ lỗi nào . Nếu một trong những lời hứa của bạn gây ra lỗi và bạn không xử lý nó, một số lời hứa sẽ không được gọi lại khiến chương trình kết thúc nhưng không thoát , vì vòng lặp sự kiện sẽ ngăn không cho đến khi thoát tất cả các nhiệm vụ, thậm chí không có cuộc gọi lại. Như nó nói trênPromise.all docs : nó sẽ từ chối ngay khi lời hứa đầu tiên từ chối.

Vì vậy, ECONNRESETlỗi của bạn không liên quan đến chính nút, là một cái gì đó với mạng của bạn đã khiến việc tìm nạp ném lỗi và sau đó ngăn vòng lặp sự kiện kết thúc. Với sửa chữa nhỏ này, bạn sẽ có thể thấy tất cả các yêu cầu được giải quyết không đồng bộ:

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");
  }
})();

Này, Pedro cảm ơn bạn đã nỗ lực giải thích. Tôi biết rằng Promise.all sẽ từ chối khi từ chối lời hứa đầu tiên xuất hiện nhưng trong hầu hết các trường hợp không có lỗi để từ chối nên toàn bộ sự việc sẽ không hoạt động.
Risto Novik

1
> Sửa chữa rằng vòng lặp sự kiện sẽ không bao giờ kết thúc cho đến khi có các tác vụ HĐH đang chờ xử lý. Nói cách khác, việc thực thi nút của bạn sẽ không bao giờ kết thúc cho đến khi có các yêu cầu HTTP đang chờ xử lý. Đây có vẻ là một điểm thú vị, nhiệm vụ hệ điều hành được quản lý thông qua libuv?
Risto Novik

Tôi đoán libuv xử lý nhiều thứ hơn liên quan đến các hoạt động (những thứ thực sự cần đa luồng). Nhưng tôi có thể sai, cần phải nhìn sâu hơn
Pedro Mutter
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.