Hành trình đi tìm cội nguồn sức mạnh Nodejs - phần 1

Hành trình đi tìm cội nguồn sức mạnh Nodejs - phần 1

Đợt này các dự án cần mình học một số thứ mới mẻ hơn như php, laravel hay golang. Nhưng với tình yêu bất tận không thể bỏ với Node, mình quyết định đào sâu thêm về core concept của Node để với bớt nỗi nhớ nhung haa.

Ở cái phần 1 của series này chúng ta sẽ bàn sơ qua một chút về cái kiến trúc cũng như thiết kế của Nodejs platform. Trước tiên là một số khái niệm cần phải biết về hệ điều hành để có thể clear hơn cho những phần tiếp theo

I/O

Trong các đoạn code mà chúng ta viết hàng ngày, I/O là các thao tác đọc ghi dữ liệu, có thể là đọc ghi từ tệp, ổ cứng,....Nếu các câu lệnh logic sẽ được load và thực thi trong RAM, tốc độ truy xuất dữ liệu trong RAM thường tính bằng nano giây và băng thông được tính bằng Gb/s thì các tác vụ đọc ghi dữ liệu từ ổ cứng hoặc từ network cần thời gian thực thi tính bằng mili giây, băng thông chỉ có Mb/s. Chính vì thế tốc độ thực thi code phụ thuộc khá nhiều vào số lượng các thao tác I/O.

Blocking I/O

Blocking I/O mô tả việc một tiến trình khi thực thi một tác vụ I/O, tiến trình đó sẽ phải tạm dừng thực thi, chờ tác vụ I/O hoàn thành và trả về kết quả, sau đó mới tiếp tục chạy tiếp. Khi áp dụng blocking I/O, chương trình sẽ không thực thi được nhiều tác vụ cùng lúc, từ đó sẽ không xử lý được một số yêu cầu thực tế. Tưởng tượng xem, nếu web server của bạn được implemented theo kiểu blocking I/O, nó sẽ không handle được nhiều connection một lúc =)) (vậy phải chờ người ngày dùng xong người kia mới được dùng à, toang :) )

Busy-waiting

Để khắc phục cái khó khăn ở trên thì một số phương pháp hay thuật toán theo hướng Non-blocking I/O được phát minh ra, cái đầu tiên có là Busy-waiting.

Theo trang baeldung, mô hình hoạt động của Busy-waiting sẽ diễn ra như hình trên. Khi tiến trình 2 cần thao tác với một resource, nó sẽ kiểm tra xem trạng thái của resource đó có khả dụng hay không, nếu resource đang được sử dụng bởi tiến trình khác thì sẽ chuyển trạng thái thành busy-waiting cho đến khi resource khả dụng. Trong trạng thái busy-waiting, tiến trình sẽ luôn thực hiện kiểm tra việc khả dụng của resource (vẫn tiếp tục running chứ không bị blocking như Blocking I/O, tuy nhiên là running việc check resource available :) ). Việc liên tục chạy kiểm tra tài nguyên này vô cùng lãng phí tài nguyên CPU => cần cải thiện.

The reactor pattern

Reactor-pattern là một kiểu thiết kế được sinh ra để cải thiện nhược điểm của busy-waiting. Workflow của thiết kế này như hình dưới đây

Dịch cái hình này sang tiếng người một chút thì nó là như thế này:

B1: Một tiến trình cần thực thi một tác vụ I/O nào đó, nó sẽ bắn một request tới thằng Event Demultiplexer (na ná một thằng quản lý sự kiện, giống nhân viên order ở nhà hàng á). Lưu ý là trong request gửi đi, tiến trình cần gửi kèm một cái handler ( để handle cái kết quả trả về sau I/O, tên khác của em nó là callback, nó chỉ định những việc cần làm sau khi nhận được kết quả trả về từ I/O). Vì việc gửi request là Non-blocking, nên khi nhận được request Event Demultiplexer sẽ ngay lập tức trả về cho process một thông điệp ok =)).

B2: Sau khi nhận được request, Event Demultiplexer sẽ tiến hành đóng gói request bao gồm các thông tin: I/O thực hiện trên resource nào, thao tác là gì (Operation => Read or Write) và handler là gì. Sau đó sẽ đẩy vào Event queue.

B3: Event loop sẽ đọc các request từ trong queue, rồi thực hiện các thao tác I/O.

B4: Kết quả trả về từ Event loop được đẩy xử lí tiếp bởi handler (hay callback function) lúc đầu ta định nghĩa.

B5: Sau khi handler xử lý xong sẽ gửi một thông điệp cho Event loop (5a) và gửi kết quả cho process (5b).

B6: Khi thực thi xong một event, Event loop sẽ block Event Demultiplexer lại để nhận một event tiếp theo và bắt đầu vòng lặp mới.

Túm cái quần lại thì thiết kế này được coi là trái tim của Nodejs, giúp nó handle multiple request chỉ với single thread, magic thật nhở =)).

Non-blocking I/O engine - libuv

Thiết kế thì tuyệt như thế, nhưng mà khi áp dụng vào thì đẻ ra đống vấn đề khác. Khi cài đặt Nodejs trên các hệ điều hành khác nhau, mỗi hệ điều hành sẽ cung cấp một giao diện riêng để xây dựng bộ Event Demutiplexer. Tuy nhiên, trong hệ điều hành Unix, hệ thống filesystem của nó không hỗ trợ các thao tác non-blocking :), mà yêu cầu của Event Demultiplexer là phải tiếp nhận các request I/O theo cách no-blocking, trả về message ngay khi nhận được request. Giải pháp được đội ngũ phát triển Node đưa ra là chạy song song một số thread khác (ngoài cái thread chạy Event loop) để có thể giả lập được cái kiểu non-blocking (quay đi quay lại thấy hề v~~~ =)). Vì cái giải pháp này mà thư viện libuv được viết bằng C được sinh ra để có thể chạy trên nhiều thread, giả lập cơ chế non-blocking giúp Node chạy được trên đa nền tảng.

Nhờ cái libuv này mà ta lý giải được tại sao "Nodejs là single thread" nhưng khi chạy lên nó lại ăn đến 2-300% CPU á.

Thoi cũng hơi dài ròi, ae đón xem phần tiếp theo nhé <3 <3

Nguồn tham khảo và hình ảnh

- Node.js Design Pattern

Did you find this article valuable?

Support Lucky Baba Yaga by becoming a sponsor. Any amount is appreciated!