Get in touch
or send us a question?
CONTACT

Bất đồng bộ trong Node.JS

thumbnail

Node.JS (gọi gọn là node) ra đời năm 2009 với mục tiêu có thể dùng ngôn ngữ JS ở back-end, đồng bộ với JS ở front-end để biến JS thành 1 ngôn ngữ full-stack thật sự. Và một trong những ưu điểm vượt trội để node được chú ý tới ngay từ ban đầu chính là khả năng xử lý bất đồng bộ, tuy nhiên ưu điểm này của node cũng chính là một trong những thách thức lớn nhất với những ai phát triển dựa trên node.

Vì sao async lại vô cùng quan trọng trong node

Khi chạy 1 chương trình bất kì, máy tính đều thực hiện các công việc tương tự nhau, hệ điều hành sẽ launch chương trình từ ổ cứng đó vào ram, khởi tạo 1 vùng nhớ cho việc chạy chương trình(heap, stack), khi chạy chương trình sẽ tương tác với các thành bên ngoài gọi là I/O(đọc file từ ổ cứng, giao tiếp với chuột/bàn phím, gọi tới và đợi kết quả từ các web server, …).

Từng thành phần ở trên sẽ ảnh hưởng tới tổng thể hiệu năng của chương trình(bottle-neck). Nếu chương trình sử dụng nhiều tính toán như encode/decode, hashing, … thì tổng thể hiệu năng phụ thuộc nhiều vào cpu, nên gọi đó là cpu-bound, nếu chương trình gọi tới nhiều web service hay database thì sẽ phụ thuộc vào tốc độ kết nối hay đáp ứng của IO, nên gọi đó là IO-bound, tương tự với memory-bound/cache-bound.

Với đặc thù của ứng dụng web phía client là gọi tới nhiều web service nên sẽ bị hạn chế rất nhiều ở IO. Nên giải quyết IO hẳn nhiên là ưu tiền hàng đầu của JS và giải pháp được JS chọn là cơ chế bất động bộ bằng event-loop.

the-cost-of-io

Câu chuyện cũng tương tự ở phía server là cần xử lý nhiều request một lúc và cũng cần phải dùng nhiều IO như đọc file hay gọi tới database.

Với các ngôn ngữ khác, thì giải pháp ở đây là sử dụng đa luồng(multi-thread), hạn chế của multi-thread là sẽ tiêu tốn nhiều tài nguyên để tạo ra các thread và sự phức tạp để đồng bộ các thread. Node thì tận lại cơ chế async để giải quyết vấn đề này.

Tóm lại ở đây, back-end tiêu tốn rất nhiều chi phí cho IO, và async chính là cách thức node dùng để giải quyết vấn đề IO một cách nhanh chóng, hiệu quả, ít tiêu tốn tài nguyên.

async hoạt động như thế nào ?

Nếu giải thích cận kẽ thì hơi phức tạp, tạm hiểu là event-loop như là một nhạc trường vận hành toàn bộ cổ máy. Thay vì chương trình phải dừng lại để đợi reponse từ phía OS khi đọc file hay database(blocking IO) thì nó sẽ thực hiện các công việc tiếp theo ở trong hàng đợi(event queue).

VD: Có 5 request tới, mỗi request cần cpu xử lý 100ms, rồi gọi tới database mất 200ms trước khi reponse về cho client.

  1. Với 1 luồng duy nhất, chúng ta sẽ cần tổng cộng thời gian là 5 * 300ms để xử lý xong cả 5 request.
  2. Hoặc chúng ta dùng 5 thread để cùng xử lý 5 request. Tổng cộng mất 300ms.
  3. Hoặc chỉ dùng 1 luồng duy nhất nhưng cộng với async.
    • Ở 300ms đầu tiên, thay vì tốn 200ms chỉ đề đợi, cpu có thể xử lý được 2 request tiếp theo và gọi liên tiếp tới database
image
  • Một cách tương đối, node xử lý các request nhanh hơn so với 1 luồng và ít tài nguyên hơn so với đa luồng.

Don’t Block the Event Loop

Tuy nhiên thì cuộc sống không dễ vậy đâu bạn ei, để tận dùng thật tốt ưu điểm của async thì chúng ta phải đảm bảo được event-loop luôn hoạt động, không bị block bởi main threadcủa V8.

Vậy thì event-loop bị block khi nào ?

Giờ chúng ta phải quay trở lại kiến trúc của Node. Một điều gây khá nhiều nhầm lẫn thì cuối cùng node là gì ? nó cũng không phải ngôn ngữ gì mới, cũng không phải là một framework theo kiểu rail của ruby, laravel của php hay django của python. Một câu trả lời tạm chấp nhận được thì nó là một platform hay runtime enviroment để chạy được code js ở back-end.

Nó bao gồm 2 thành phần chính là V8 engine để chạy code js và thư viện libuv để xử lý các lời gọi bất đồng bộ. Ở đây lại có nhiều lẫn lộn node chạy đơn luồng hay đa luồng ? Một cách chính xác thì code JS được xử lý bằng 1 luồng duy nhất chính là V8 engine(main thread), còn các thứ chạy bên dưới bởi libuv thì đa luồng(worker thread).

“everything runs in parallel except your code.”

image

Có thể tham khảo cách thức hoạt động của node qua ví dụ này

Theo đó, trong code của chúng ta sẽ được thực hiện tuần tự từ trên xuống dưới như bình thường trong main thread, khi gặp những lời gọi bất đồng bộ thì sẽ đẩy sang node api mà ở dưới sẽ được xử lý đa luồng(tạm hiểu). Mỗi lời gọi bất đồng bộ sẽ được đăng kí 1 hàm xử lý sau đó gọi là callback, hàm callback sẽ không được thực hiện trực tiếp mà sẽ được đẩy vào callback queue và đợi ở đây. Chỉ khi nào được event-loop đẩy vào main thread của V8 thì hàm này mới được xử lý.

Và chỉ khi nào main thread đã xử lý hết code thì trả lại quyền kiểm soát cho event-loop. Lúc này event-loop mới pick hàm callback trong callback-queue để đẩy nó vào main thread xử lý và trả lại quyền cho main thread.

Nếu main thread phải xử lý 1 tác vụ cần tính toán lâu thì đồng nghĩa là event-loop sẽ bị block. Điều này làm cho node khả năng đáp ứng của server bị giảm đáng kể.

Tóm lại một điều quan trọng nên nhớ ở đây là Don’t Block the Event Loop

multithread

Nhưng rất may là tới bản node 10.5 đã thử nghiệm và 11.7 đã chính thức hỗ cơ chế multithread.

Như đã nói ở trên, multithread thì có vấn đề của nó, nên mình nghĩ tinh thần của multithread trong node là hạn chế tối thiểu việc dùng thread, và chỉ dùng để tránh block event-loop. Tuy nhiên đây là một feature mới nên cũng cần thời gian để có cách áp dụng hợp lý nhất trong thực tế.

tóm lại

Kiến trúc bất đồng bộ giúp node có khả năng xử lý được một lượng lớn request tới server với một tài nguyên hạn chế. Tuy nhiên điều đó chỉ có thể làm được nếu ta hiểu được cơ chế hoạt động của event-loop để tránh block event-loop.