Khi bạn mới học Node.js, một trong những câu hỏi đầu tiên mà bạn có thể đặt ra là: "Làm thế nào mà Node.js có thể xử lý hàng ngàn Request đồng thời với chỉ một single-thread và một event loop?". Đây là một câu hỏi đáng suy ngẫm bởi vì nó liên quan trực tiếp đến hiệu suất và khả năng mở rộng của ứng dụng của bạn.
I. Node.js: Cơ bản về cách hoạt động
1.1 Single-thread và Event Loop
Node.js sử dụng một single-thread kết hợp với event loop để xử lý các Request với cơ chế non-blocking. Điều này có nghĩa là mỗi khi một Request đến, Node.js sẽ xử lý nó và không chờ đợi Request đó hoàn thành trước khi chuyển sang Request khác.
Ví dụ
Mình đang sống tại Nhật khi mình đi lên bệnh viện thăm khám thì quá trình sơ khám sẽ là: Đầu tiên là lấy số (ví dụ số của mình là 50 và từ đây cho đến khi buổi khám kết thúc thì id của mình sẽ là 50.)
// Tạo một request với tên là độc nhất Unique như sau
const request50 = { keyWork: 'Tôi bị đau bụng dữ dội 3 ngày, không sốt....'
}
Thường thì mới lấy số xong y tá sẽ đọc luôn số của mình luôn. Ý tá hỏi nhẹ 1-2 câu biết ngay mình đau bụng (cái này siêu nhanh mất chưa tới 1p) và đưa cho mình tờ form khoa nội tổng hợp và bảo mình ra ghế điền và y tá sẽ lập tức kêu số khác.
// Khi có request thì check nó luôn. Mapping với keyWork mà người bệnh cung cấp thì sẽ biết ngay phải gọi tới Controller nào (tức là đưa cho form nào ấy)
const router = { 'api/Nội Tổng hợp': đưa form khoa nội tổng hợp và bảo bệnh nhân ra ghế điền 'api/Ngoại Tổng Hợp': đưa form khoa nội tổng hợp và bảo bệnh nhân ra ghế điền 'api/Khoa Nhi': đưa form khoa nội tổng hợp và bảo bệnh nhân ra ghế điền ...
}
Sau khi điền xong thì mình sẽ bỏ lại một cái Box Queue (Ai bỏ vào trước thì sẽ được gọi tên trước).
Khi y tá lấy đúng tờ khảo sát đọc qua phán đoán đây khả năng cao là đại tràng rồi... lập kêu mình đi qua phòng khám nội tổng hợp khám....
Vậy qua ví dụ trên ta thử nghĩ xem Y tá chính là Server Nodejs trong khi một bệnh nhân đang điện form khảo sát thì không chờ mà gọi ngay Bệnh nhân khác lên để xử lý. Trong code sẽ là khi truy vẫn Database thì không chờ nó xong mà sẽ sang xử lý request khác.
Query thì cũng có nhanh chậm vậy nếu bệnh nhân nào mà điền form nhanh thì sẽ nộp vào Box queue sớm hơn và được trả kết quả sớm hơn.
Tuy nhiên Y tá có một luật mà phải tuân theo đó là non-blocking có nghĩa là ko bao giờ chờ bệnh nhân suy nghĩ nếu mà bệnh nhân ko trả lời được liền thì sẽ đưa cho họ một tờ giấy để ghi câu trả lời vào giấy và bỏ vào thùng bên cạnh...
Vậy là chỉ cần 1 cô y tá thôi mà ko bao giờ ai phải đợi bất kỳ ai. Cứ hễ ai trả lời xong hoặc tự điền xong đơn thì sẽ được vào khám trước... Quá trình siêu nhanh.
Bảng điện tử hiển thị số thứ tự gồm 2 list:
Số hiện tại: 50 (Mỗi lần chỉ xử lý đúng 1 số tuy nhiên vì mọi người đều đã điền form nên xử lý rất nhanh chỉ nhì qua check check là xong -> nó là callStack)
Hàng chờ: 12 99 30 11 40 55... (là những người điềm xong form thì đặt vào Box Queue -> nó tương đương với callBack Queue trong nodejs. Các bạn thấy đó nó ko theo thứ tự gì cả )
Xảnh chờ: mọi người ngồi đây để điền đơn (Tương đương với Database đang query dữ liệu. Có kết quả thì mới được cho vào hàng chờ.)
1.2 Single-thread so với Multi-thread
Trong hầu hết các ngôn ngữ lập trình khác, mỗi Request sẽ được xử lý bởi một thread riêng biệt. Điều này có thể dẫn đến việc tiêu tốn nhiều tài nguyên hệ thống (CPU, bộ nhớ) khi số lượng Request tăng lên.
Ví dụ
Cái này mình thấy khá giống lấy bảo hiểm thất nghiệp ở VN. Xử lý đa luồng mà lâu vãi linh hồn.
Đầu tiên có 30 bàn xử lý bảo hiểm thất nghiệp (1 bàn là một thread độc lập) gọi tới số nào thì người đó vào để xử lý. Gọi tới tên mình thì mình lên tuy nhiên trong lúc mình điền đơn thì người ta ngồi chơi (có người còn lướt facebook) và đợi mình điền xong xử lý xong thì mới gọi người khác (một số như vậy xử lý tầm 30-40p ôi chao nhớ tới cái hồi nhận bảo hiểm thất nghiệp đi từ sáng sớm mặt trời chưa lên mà tới gần chiều mới xong).
1.3 Tuy nhiên cái gì cũng có điểm yếu của nó
Có bạn sẽ nói ngay ông mới qua Nhật vài bữa là bày trò chê VN rồi.... Thực ra không phải cái nào cũng có điểm mạnh và điểm yếu cả.
Các bạn thấy đó nếu sử dụng Single-thread và Event Loop thì siêu nhanh tuy nhiên các bạn có để ý là bệnh nhân ai cũng tự mình điền đơn được không. Đó chính là mấu chốt thực ra nếu Y tá không phải xử lý thì xử lý cũng đẩy cho Bệnh Nhân thôi. Và cụ thể bệnh nhân đây là ai chính là Database của chúng ta chứ đâu vào đây nữa. Có thể xử lý 1000 yêu cầu cùng một lúc không có nghĩa là thực hiện xong 1000 yêu cầu trong 1 lúc. Mọi thứ vẫn phụ thuộc vào Database phải đáp ứng được nhu cầu của Server chứ không thì cũng nghẽn cổ chai.
Với ví dụ 2 thì đúng là chỉ xử lý được 30 người cùng 1 lúc tuy nhiên vì phải công nhận là trình độ dân trí của mình sơ với Nhật có vẻ vẫn còn chưa cao lắm nên để tự viết cũng khó mà đôi khi sẽ là Nhân viên của BHXH sẽ điền form giúp mình bằng cách nhập vào máy tính và hỏi trực tiếp mình luôn. Database mình cũng chỉ có thế giờ có 1000 yêu cầu một lúc cũng đâu có được.
Tuy nhiên càng ngày yêu cầu càng cao Dịch vụ không nhanh là đánh giá 1 sao ngay. Nhiều lúc ngồi chờ ông phía trước lắp ba lắp bắp, quên giấy này, sót giấy kia,... điền mãi không xong cái form mà mình cũng bực trong khi có ông thì điền cái vèo là xong.
Việc đó chả khác gì vào Facebook chỉ đơn giản là load cái newsfeed thôi mà phải chờ user khác load toàn bộ danh sách các hình ảnh của họ ra. Chỉ vì mình login vào sau và phải đợi ông trước load xong mới tới mình... Có vẻ như trong thực tế các App ko hoạt động như vậy nhỉ.
Trên đây chỉ là những ví dụ đơn giản cho các bạn giễ hình dung thôi nên đôi lúc bản chất nó ko đúng 100% nhưng đại khái nó là như thế.
II. Xử lý Request trong Web App
2.1 Quy trình xử lý thông thường
Nếu bạn mới bắt đầu với phát triển Web App, có thể bạn nghĩ rằng mọi ứng dụng đều hoạt động theo quy trình sau:
Người dùng thực hiện hành động │ v
Ứng dụng bắt đầu xử lý hành động └──> vòng lặp ... └──> xử lý kết thúc vòng lặp └──> gửi kết quả cho người dùng
Tuy nhiên, thực tế không phải vậy, đặc biệt đối với các Web App sử dụng cơ sở dữ liệu làm back-end. Các Web App thực sự hoạt động như sau:
Người dùng thực hiện hành động │ v
Ứng dụng bắt đầu xử lý hành động └──> gửi Request tới cơ sở dữ liệu └──> không làm gì cho tới khi Request hoàn tất Request hoàn tất └──> gửi kết quả cho người dùng
Ở đây, phần lớn thời gian xử lý của phần mềm sẽ được dành để chờ cơ sở dữ liệu trả về kết quả, trong đó CPU sẽ không được sử dụng (0% CPU).
2.2 Node.js và khả năng xử lý hàng ngàn Request
Như đã giải thích ở trên, Node.js sử dụng một single-thread kết hợp với event loop để xử lý Request. Khi một Request đến, Node.js sẽ gửi Request đó tới cơ sở dữ liệu và không chờ đợi kết quả trả về mà chuyển sang xử lý Request tiếp theo. Điều này đảm bảo việc sử dụng tài nguyên hệ thống (CPU, bộ nhớ) một cách hiệu quả.
Ví dụ
Giả sử chúng ta có một Web App sử dụng Node.js để xử lý các Request đến từ người dùng. Khi một người dùng Request truy vấn dữ liệu từ cơ sở dữ liệu, ứng dụng sẽ hoạt động như sau:
Request truy vấn dữ liệu từ người dùng │ v
Node.js bắt đầu xử lý Request └──> gửi Request tới cơ sở dữ liệu └──> không chờ đợi kết quả trả về
Chuyển sang xử lý Request tiếp theo
Khi cơ sở dữ liệu trả về kết quả, Node.js sẽ xử lý (ở đây ám chỉ chỉ là một số xử lý đơn giản như là tạo Object for vài vòng vớ vẩn, chứ nếu bạn blocking callStack lại thì cũng Ăn hành ngay) và gửi kết quả đó tới người dùng. Điều này đảm bảo việc sử dụng tài nguyên hệ thống một cách hiệu quả và cho phép Node.js xử lý hàng ngàn Request đồng thời.
Kết luận
Node.js là một môi trường chạy mã JavaScript trên máy chủ và cũng là một công cụ mạnh mẽ để xây dựng các ứng dụng mạng hiệu suất cao, khả năng mở rộng và tối ưu hóa tài nguyên. Bằng cách sử dụng một single-thread và event loop, Node.js có thể xử lý hàng ngàn Request đồng thời mà không cần tăng bộ nhớ và CPU.
Hy vọng rằng bài viết này đã giúp bạn hiểu rõ hơn về cách thức hoạt động của Node.js và làm thế nào nó có thể xử lý được hàng ngàn Request đồng thời.