Nói chung, Node.js xử lý 10.000 yêu cầu đồng thời như thế nào?


395

Tôi hiểu rằng Node.js sử dụng một chuỗi đơn và một vòng lặp sự kiện để xử lý các yêu cầu chỉ xử lý một yêu cầu tại một thời điểm (không chặn). Tuy nhiên, làm thế nào nó hoạt động, cho phép nói 10.000 yêu cầu đồng thời. Vòng lặp sự kiện sẽ xử lý tất cả các yêu cầu? Điều đó có mất quá nhiều thời gian không?

Tôi không thể hiểu (làm thế nào) nó có thể nhanh hơn một máy chủ web đa luồng. Tôi hiểu rằng máy chủ web đa luồng sẽ tốn kém hơn về tài nguyên (bộ nhớ, CPU), nhưng liệu nó có còn nhanh hơn không? Tôi có lẽ sai; vui lòng giải thích cách đơn luồng này nhanh hơn trong nhiều yêu cầu và những gì nó thường làm (ở mức cao) khi phục vụ nhiều yêu cầu như 10.000.

Và ngoài ra, liệu đơn luồng có quy mô tốt với số tiền lớn đó không? Xin lưu ý rằng tôi mới bắt đầu tìm hiểu Node.js.


5
Bởi vì hầu hết các công việc (di chuyển dữ liệu xung quanh) không liên quan đến CPU.
OrangeDog

5
Cũng lưu ý rằng chỉ vì chỉ có một luồng thực thi Javascript, không có nghĩa là không có nhiều luồng khác hoạt động.
OrangeDog

Câu hỏi này quá rộng hoặc trùng lặp với nhiều câu hỏi khác.
OrangeDog


Cùng với phân luồng đơn, Node.js thực hiện một cái gì đó gọi là "không chặn I / O". Đây là nơi tất cả các phép thuật được thực hiện
Anand N

Câu trả lời:


764

Nếu bạn phải hỏi câu hỏi này thì có lẽ bạn không quen với những gì hầu hết các ứng dụng / dịch vụ web làm. Bạn có thể nghĩ rằng tất cả các phần mềm làm điều này:

user do an action
       
       v
 application start processing action
   └──> loop ...
          └──> busy processing
 end loop
   └──> send result to user

Tuy nhiên, đây không phải là cách các ứng dụng web, hoặc thực sự là bất kỳ ứng dụng nào có cơ sở dữ liệu làm back-end, hoạt động. Các ứng dụng web làm điều này:

user do an action
       
       v
 application start processing action
   └──> make database request
          └──> do nothing until request completes
 request complete
   └──> send result to user

Trong trường hợp này, phần mềm dành phần lớn thời gian chạy bằng 0% thời gian CPU chờ cơ sở dữ liệu quay trở lại.

Ứng dụng mạng đa luồng:

Các ứng dụng mạng đa luồng xử lý khối lượng công việc trên như thế này:

request ──> spawn thread
              └──> wait for database request
                     └──> answer request
request ──> spawn thread
              └──> wait for database request
                     └──> answer request
request ──> spawn thread
              └──> wait for database request
                     └──> answer request

Vì vậy, luồng dành phần lớn thời gian của họ bằng cách sử dụng CPU 0% chờ cơ sở dữ liệu trả về dữ liệu. Trong khi làm như vậy, họ phải phân bổ bộ nhớ cần thiết cho một luồng bao gồm một ngăn chương trình hoàn toàn riêng biệt cho mỗi luồng, v.v. Ngoài ra, họ sẽ phải bắt đầu một luồng mà không tốn kém như khi bắt đầu một quá trình đầy đủ vẫn chưa chính xác rẻ.

Vòng lặp sự kiện Singlethreaded

Vì chúng tôi dành phần lớn thời gian để sử dụng CPU 0%, tại sao không chạy một số mã khi chúng tôi không sử dụng CPU? Theo cách đó, mỗi yêu cầu vẫn sẽ có cùng thời lượng CPU như các ứng dụng đa luồng nhưng chúng ta không cần phải bắt đầu một chuỗi. Vì vậy, chúng tôi làm điều này:

request ──> make database request
request ──> make database request
request ──> make database request
database request complete ──> send response
database request complete ──> send response
database request complete ──> send response

Trong thực tế, cả hai phương pháp đều trả về dữ liệu với độ trễ gần như nhau vì đó là thời gian đáp ứng cơ sở dữ liệu chi phối quá trình xử lý.

Ưu điểm chính ở đây là chúng ta không cần phải tạo ra một chủ đề mới vì vậy chúng ta không cần phải làm nhiều và rất nhiều malloc sẽ làm chúng ta chậm lại.

Phép thuật, luồng vô hình

Điều có vẻ bí ẩn là làm thế nào cả hai cách tiếp cận ở trên quản lý để chạy khối lượng công việc "song song"? Câu trả lời là cơ sở dữ liệu được xâu chuỗi. Vì vậy, ứng dụng đơn luồng của chúng tôi thực sự đang thúc đẩy hành vi đa luồng của một quy trình khác: cơ sở dữ liệu.

Trường hợp cách tiếp cận singlethreaded thất bại

Một ứng dụng singlethreaded thất bại lớn nếu bạn cần thực hiện nhiều phép tính CPU trước khi trả lại dữ liệu. Bây giờ, tôi không có nghĩa là một vòng lặp để xử lý kết quả cơ sở dữ liệu. Điều đó vẫn chủ yếu là O (n). Ý tôi là những thứ như thực hiện biến đổi Fourier (ví dụ mã hóa mp3), dò tia (kết xuất 3D), v.v.

Một cạm bẫy khác của các ứng dụng đơn lẻ là nó sẽ chỉ sử dụng một lõi CPU duy nhất. Vì vậy, nếu bạn có một máy chủ lõi tứ (không phổ biến ngày nay) thì bạn không sử dụng 3 lõi khác.

Trường hợp phương pháp đa luồng không thành công

Một ứng dụng đa luồng không thành công lớn nếu bạn cần phân bổ nhiều RAM cho mỗi luồng. Đầu tiên, bản thân việc sử dụng RAM có nghĩa là bạn không thể xử lý nhiều yêu cầu như một ứng dụng đã đọc. Tệ hơn, malloc là chậm. Phân bổ nhiều và rất nhiều đối tượng (vốn phổ biến cho các khung web hiện đại) có nghĩa là chúng ta có khả năng sẽ chậm hơn các ứng dụng đã đọc. Đây là nơi mà node.js thường giành chiến thắng.

Một trường hợp sử dụng cuối cùng làm cho đa luồng trở nên tồi tệ hơn là khi bạn cần chạy một ngôn ngữ kịch bản lệnh khác trong luồng của mình. Đầu tiên bạn thường cần malloc toàn bộ thời gian chạy cho ngôn ngữ đó, sau đó bạn cần malloc các biến được sử dụng bởi tập lệnh của bạn.

Vì vậy, nếu bạn đang viết các ứng dụng mạng bằng C hoặc go hoặc java thì chi phí phân luồng thường sẽ không quá tệ. Nếu bạn đang viết một máy chủ web C để phục vụ PHP hoặc Ruby thì việc viết một máy chủ nhanh hơn bằng javascript hoặc Ruby hoặc Python là rất dễ dàng.

Phương pháp lai

Một số máy chủ web sử dụng phương pháp lai. Ví dụ, Nginx và Apache2 triển khai mã xử lý mạng của họ như một chuỗi các vòng lặp sự kiện. Mỗi luồng chạy một vòng lặp sự kiện đồng thời xử lý các yêu cầu đơn luồng nhưng các yêu cầu được cân bằng tải giữa nhiều luồng.

Một số kiến ​​trúc đơn luồng cũng sử dụng phương pháp lai. Thay vì khởi chạy nhiều luồng từ một tiến trình đơn lẻ, bạn có thể khởi chạy nhiều ứng dụng - ví dụ: 4 máy chủ node.js trên máy lõi tứ. Sau đó, bạn sử dụng một bộ cân bằng tải để phân tán khối lượng công việc giữa các quy trình.

Trong thực tế, hai cách tiếp cận là hình ảnh phản chiếu giống hệt nhau về mặt kỹ thuật của nhau.


106
Đây là lời giải thích tốt nhất cho nút tôi đã đọc cho đến nay. "Ứng dụng đơn luồng đó thực sự đang thúc đẩy hành vi đa luồng của một quy trình khác: cơ sở dữ liệu." Đã thực hiện công việc
kenobiwan

Điều gì sẽ xảy ra nếu máy khách thực hiện nhiều yêu cầu trong nút, ví dụ như lấy tên và sửa đổi nó, và nói rằng thao tác này được đẩy đến máy chủ để xử lý rất nhanh bởi nhiều khách hàng. Làm thế nào tôi có thể xử lý một kịch bản như vậy?
Remario

3
@CaspainCaldion Nó phụ thuộc vào ý của bạn bởi rất nhanh và rất nhiều khách hàng. Như vậy, node.js có thể xử lý lên tới 1000 yêu cầu mỗi giây và tốc độ chỉ giới hạn ở tốc độ của card mạng của bạn. Lưu ý rằng đó là 1000 yêu cầu mỗi giây không phải máy khách được kết nối đồng thời. Nó có thể xử lý 10000 khách hàng đồng thời mà không có vấn đề. Nút thắt thực sự là card mạng.
slebetman

1
@slebetman, giải thích tốt nhất bao giờ hết. mặc dù vậy, nếu tôi có thuật toán Machine Learning cung cấp một số thông tin và cung cấp kết quả tương ứng, tôi nên sử dụng phương pháp tiếp cận đa luồng hoặc đơn luồng
Ganesh Karewad

5
@GaneshKarewad Thuật toán sử dụng CPU, dịch vụ (cơ sở dữ liệu, API REST, v.v.) sử dụng I / O. Nếu AI là một thuật toán được viết bằng js thì bạn nên chạy nó trong một luồng hoặc tiến trình khác. Nếu AI là một dịch vụ chạy trên một máy tính khác (như dịch vụ AI của Amazon hoặc Google hoặc IBM) thì hãy sử dụng một kiến ​​trúc luồng đơn.
slebetman

46

Những gì bạn dường như đang nghĩ là hầu hết việc xử lý được xử lý trong vòng lặp sự kiện nút. Node thực sự trang trại tắt công việc I / O cho chủ đề. Các hoạt động I / O thường nhận các đơn đặt hàng có cường độ dài hơn các hoạt động của CPU, vậy tại sao CPU lại chờ đợi điều đó? Bên cạnh đó, HĐH có thể xử lý các tác vụ I / O rất tốt. Trên thực tế, vì Node không chờ đợi xung quanh nên nó đạt được mức sử dụng CPU cao hơn nhiều.

Theo cách tương tự, hãy nghĩ về NodeJS như một người phục vụ nhận đơn đặt hàng của khách hàng trong khi các đầu bếp I / O chuẩn bị chúng trong bếp. Các hệ thống khác có nhiều đầu bếp, họ nhận đặt hàng của khách hàng, chuẩn bị bữa ăn, dọn bàn và chỉ sau đó tham dự cho khách hàng tiếp theo.


5
Cảm ơn sự tương tự nhà hàng! Tôi tìm thấy sự tương tự và các ví dụ trong thế giới thực để học hỏi dễ dàng hơn nhiều.
LaVache

13

Tôi hiểu rằng Node.js sử dụng một chuỗi đơn và một vòng lặp sự kiện để xử lý các yêu cầu chỉ xử lý một yêu cầu tại một thời điểm (không chặn).

Tôi có thể hiểu nhầm những gì bạn đã nói ở đây, nhưng "mỗi lần một" có vẻ như bạn có thể không hiểu đầy đủ về kiến ​​trúc dựa trên sự kiện.

Trong một kiến ​​trúc ứng dụng "thông thường" (không theo hướng sự kiện), quá trình này dành rất nhiều thời gian để chờ đợi điều gì đó xảy ra. Trong một kiến ​​trúc dựa trên sự kiện như Node.js, quá trình không chỉ chờ đợi, nó có thể tiếp tục với công việc khác.

Ví dụ: bạn nhận được kết nối từ máy khách, bạn chấp nhận nó, bạn đọc các tiêu đề yêu cầu (trong trường hợp http), sau đó bạn bắt đầu hành động theo yêu cầu. Bạn có thể đọc phần yêu cầu, nói chung bạn sẽ gửi một số dữ liệu trở lại máy khách (đây là một sự đơn giản hóa có chủ ý của thủ tục, chỉ để chứng minh luận điểm).

Ở mỗi giai đoạn này, phần lớn thời gian được dành để chờ một số dữ liệu đến từ đầu kia - thời gian thực tế dành cho xử lý trong luồng JS chính thường khá tối thiểu.

Khi trạng thái của một đối tượng I / O (chẳng hạn như kết nối mạng) thay đổi để nó cần xử lý (ví dụ: dữ liệu được nhận trên một ổ cắm, một ổ cắm có thể ghi được, v.v.), luồng JS chính của Node.js được đánh dấu bằng một danh sách các mặt hàng cần được xử lý.

Nó tìm thấy cấu trúc dữ liệu có liên quan và phát ra một số sự kiện trên cấu trúc đó khiến cho các cuộc gọi lại được chạy, xử lý dữ liệu đến hoặc ghi thêm dữ liệu vào ổ cắm, v.v. Một khi tất cả các đối tượng I / O cần xử lý đã được xử lý đã được xử lý, luồng JS chính của Node.js sẽ đợi một lần nữa cho đến khi được thông báo rằng có nhiều dữ liệu hơn (hoặc một số thao tác khác đã hoàn thành hoặc hết thời gian).

Lần sau khi nó được đánh thức, nó cũng có thể là do một đối tượng I / O khác cần được xử lý - ví dụ như một kết nối mạng khác. Mỗi lần, các cuộc gọi lại có liên quan được chạy và sau đó nó quay trở lại giấc ngủ để chờ điều gì đó khác xảy ra.

Điểm quan trọng là việc xử lý các yêu cầu khác nhau được xen kẽ, nó không xử lý một yêu cầu từ đầu đến cuối và sau đó chuyển sang yêu cầu tiếp theo.

Theo tôi, ưu điểm chính của điều này là yêu cầu chậm (ví dụ: bạn đang cố gửi 1MB dữ liệu phản hồi đến thiết bị điện thoại di động qua kết nối dữ liệu 2G hoặc bạn đang thực hiện truy vấn cơ sở dữ liệu rất chậm) đã thắng ' T chặn những cái nhanh hơn.

Trong một máy chủ web đa luồng thông thường, thông thường bạn sẽ có một luồng cho mỗi yêu cầu được xử lý và nó sẽ xử lý CHỈ yêu cầu đó cho đến khi hoàn thành. Điều gì xảy ra nếu bạn có nhiều yêu cầu chậm? Bạn kết thúc với rất nhiều chủ đề của bạn xung quanh xử lý các yêu cầu này và các yêu cầu khác (có thể là các yêu cầu rất đơn giản có thể được xử lý rất nhanh) được xếp hàng phía sau chúng.

Có rất nhiều hệ thống dựa trên sự kiện khác ngoài Node.js và chúng có xu hướng có những ưu điểm và nhược điểm tương tự so với mô hình thông thường.

Tôi sẽ không cho rằng các hệ thống dựa trên sự kiện nhanh hơn trong mọi tình huống hoặc với mọi khối lượng công việc - chúng có xu hướng hoạt động tốt đối với khối lượng công việc bị ràng buộc I / O, không tốt cho các hệ thống bị ràng buộc CPU.


12

Các bước xử lý mô hình vòng lặp sự kiện đơn luồng:

  • Khách hàng Gửi yêu cầu đến Máy chủ Web.

  • Máy chủ web Node JS duy trì bên trong nhóm luồng có giới hạn để cung cấp dịch vụ cho các yêu cầu của máy khách.

  • Máy chủ web Node JS nhận các yêu cầu đó và đặt chúng vào hàng đợi. Nó được biết đến như là sự kiện Queue xếp hàng.

  • Máy chủ web Node JS bên trong có một Thành phần, được gọi là Vòng lặp Sự kiện Vòng lặp. Tại sao nó có tên này là nó sử dụng vòng lặp không xác định để nhận yêu cầu và xử lý chúng.

  • Vòng lặp sự kiện chỉ sử dụng Chủ đề duy nhất. Nó là trung tâm chính của Mô hình xử lý nền tảng Node JS.

  • Vòng lặp sự kiện kiểm tra bất kỳ Yêu cầu khách hàng nào được đặt trong Hàng đợi sự kiện. Nếu không thì chờ đợi các yêu cầu đến vô thời hạn.

  • Nếu có, sau đó chọn một Yêu cầu của Khách hàng từ Hàng đợi Sự kiện

    1. Bắt đầu quá trình mà Khách hàng yêu cầu
    2. Nếu Yêu cầu khách hàng đó không yêu cầu bất kỳ hoạt động chặn IO nào, thì hãy xử lý mọi thứ, chuẩn bị phản hồi và gửi lại cho khách hàng.
    3. Nếu Yêu cầu khách hàng đó yêu cầu một số Hoạt động chặn IO như tương tác với Cơ sở dữ liệu, Hệ thống tệp, Dịch vụ bên ngoài thì nó sẽ theo cách tiếp cận khác
  • Kiểm tra tính khả dụng của Chủ đề từ Nhóm chủ đề nội bộ
  • Chọn một Chủ đề và gán Yêu cầu Khách hàng này cho chủ đề đó.
  • Chủ đề đó chịu trách nhiệm thực hiện yêu cầu đó, xử lý nó, thực hiện Chặn các hoạt động IO, chuẩn bị phản hồi và gửi lại cho Vòng lặp sự kiện

    @Rambabu Posa giải thích rất độc đáo để giải thích thêm hãy ném Liên kết này


sơ đồ đưa ra trong bài đăng trên blog đó dường như là sai, những gì họ đã đề cập trong bài viết đó là không hoàn toàn chính xác.
rranj

11

Thêm vào câu trả lời của slebetman: Khi bạn nói Node.JScó thể xử lý 10.000 yêu cầu đồng thời, về cơ bản chúng là các yêu cầu không chặn, tức là các yêu cầu này chủ yếu liên quan đến truy vấn cơ sở dữ liệu.

Bên trong, event loopcủa Node.JSlà xử lý một thread pool, trong đó mỗi chủ đề xử lý một non-blocking requestvà vòng lặp sự kiện tiếp tục lắng nghe yêu cầu nhiều hơn sau khi uỷ thác công việc cho một trong những chủ đề của thread pool. Khi một trong các luồng hoàn thành công việc, nó sẽ gửi tín hiệu đến event loopnó đã hoàn thành aka callback. Event loopsau đó xử lý cuộc gọi lại này và gửi phản hồi lại.

Vì bạn chưa quen với NodeJS, hãy đọc thêm nextTickđể hiểu cách vòng lặp sự kiện hoạt động bên trong. Đọc blog trên http://javascriptissexy.com , chúng thực sự hữu ích cho tôi khi tôi bắt đầu với JavaScript / NodeJS.


2

Thêm vào câu trả lời của slebetman để rõ hơn về những gì xảy ra trong khi thực thi mã.

Nhóm luồng nội bộ trong nodeJs chỉ có 4 luồng theo mặc định. và nó không giống như toàn bộ yêu cầu được đính kèm vào một luồng mới từ nhóm luồng, toàn bộ thực thi yêu cầu xảy ra giống như bất kỳ yêu cầu bình thường nào (không có bất kỳ tác vụ chặn nào), chỉ là bất cứ khi nào một yêu cầu có bất kỳ hoạt động nặng hoặc hoạt động nặng như db cuộc gọi, thao tác tệp hoặc yêu cầu http, tác vụ được xếp hàng vào nhóm luồng nội bộ được cung cấp bởi libuv. Và vì nodeJs cung cấp 4 luồng trong nhóm luồng nội bộ theo mặc định, mỗi yêu cầu đồng thời thứ 5 hoặc tiếp theo sẽ đợi cho đến khi một luồng tự do và một khi các thao tác này kết thúc, cuộc gọi lại được đẩy đến hàng đợi gọi lại. và được chọn bởi vòng lặp sự kiện và gửi lại phản hồi.

Bây giờ đến đây một thông tin khác rằng hàng đợi một lần gọi lại, có rất nhiều hàng đợi.

  1. Hàng đợi NextTick
  2. Hàng đợi nhiệm vụ vi mô
  3. Hàng đợi thời gian
  4. Hàng đợi gọi lại IO (Yêu cầu, Tệp op, db op)
  5. Hàng đợi bình chọn IO
  6. Kiểm tra hàng đợi pha hoặc SetImmediate
  7. đóng xử lý xếp hàng

Bất cứ khi nào một yêu cầu xuất hiện, mã sẽ được thực thi theo thứ tự các cuộc gọi lại được xếp hàng.

Nó không giống như khi có một yêu cầu chặn nó được gắn vào một luồng mới. Chỉ có 4 chủ đề theo mặc định. Vì vậy, có một hàng đợi khác xảy ra ở đó.

Bất cứ khi nào trong mã, một quá trình chặn như đọc tệp xảy ra, sau đó gọi một hàm sử dụng luồng từ nhóm luồng và sau đó khi thao tác được thực hiện, cuộc gọi lại được chuyển đến hàng đợi tương ứng và sau đó được thực hiện theo thứ tự.

Mọi thứ được xếp hàng dựa trên loại gọi lại và được xử lý theo thứ tự được đề cập ở trên.

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.