Node.js thích hợp với những ứng dụng nào

Node.js thích hợp với những ứng dụng nào
Photo by Juanjo Jaramillo from Unsplash

Thời gian gần đây tôi thường làm việc với Node.js ở phía server (combo Node.js + React đi muôn nơi). Không thể phủ nhận Node.js đem đến rất nhiều lợi ích cho các lập trình viên web như tôi. Thế nhưng nó có thật sự thần thánh như vậy hay không, Node.js có thích hợp để mang đi muôn nơi hay không?

Trong bài viết này tôi sẽ trình bày một số hiểu biết của tôi về Node.js và những ứng dụng của nó trong thực tế.

Giới thiệu

JavaScript đã trở nên rất phổ biến và bản thân ngôn ngữ cũng được cập nhật rất nhiều. Ngày nay, các ứng dụng web đã rất khác so với trước kia. Tầm hơn chục năm trước đây, rất ít người nghĩ đến những gì chúng ta có thể làm được bằng JavaScript cả ở phía server cũng như trên trình duyệt như bây giờ. Trước khi JavaScript có một sức mạnh ghê gớm như vậy, nhiều ứng dụng trên nền web phải hoạt động dựa vào Flash hoặc Java Applet.

Trước khi đi sâu vào Node.js, thì bản thân việc sử dụng cùng một ngôn ngữ (JavaScript) cho cả server và trình duyệt sẽ đem lại lợi ích rất lớn. Chúng ta có thể dễ dàng lập trình hơn, đồng thời việc sử dụng dữ liệu cũng đồng nhất hơn, cho phép chúng ta tối ưu hóa được các tài nguyên.

Đây là những lợi ích của bản thân ngôn ngữ JavaScript đem lại chứ không phải là do Node.js (gián tiếp phần nào Node.js cũng có vai trò trong đó, vì Node.js là môi trường hoạt động của JavaScript trên server). Tuy nhiên, trong nội dung bài viết này tôi muốn tập trung vào Node.js là chính nên sẽ không phân tích sâu vào những lợi ích của JavaScript.

Node.js được mô tả trên trang chủ như sau:

Node.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine.

Tác giả của Node.js mong muốn tạo ra một ứng dụng real time, theo lời anh ta là “lấy cảm hứng từ các ứng dụng như Gmail”. Thực sự là Node.js đã cho các lập trình viên một công cụ làm việc với kiểu lập trình non-blocking và event-driven. Sau rất nhiều năm làm việc mô hình website truyền thống (vốn tập trung vào việc nhập và phản hồi truy vấn), giờ đây lập trình viên có thêm lựa chọn về các ứng dụng web real-time và kết nối hai chiều.

Có thể nói ngắn gọn rằng, Node.js thực sự là lựa chọn số 1 cho những ứng dụng real time, đặc biệt là các ứng dụng cần kết nối hai chiều bằng việc sử dụng cơ chế push thông qua websocket. Khác với mô hình truyền thống, những ứng dụng này cho phép client và server kết nối hai chiều với nhau và có thể trao đổi thông tin tốt hơn rất nhiều. Điểm đặc biệt khiến nó nổi bật hơn là mọi giao tiếp này vẫn hoàn toàn dựa trên HTML, CSS, JS và server sẽ chạy ở cổng 80 giống như HTTP truyền thống.

Trước khi Node.js ra đời, đã có rất nhiều kỹ thuật khác nhau được sử dụng cho những ứng dụng kiểu này, ví dụ Flash và Java Applet. Thực tế là những kỹ thuật này sẽ tải và thực thi ứng dụng trên một môi trường hoàn toàn độc lập với trang web và sẽ sử dụng các giao thức khác để kết nối client và server. Và thực tế cũng đã chứng minh những hạn chế của kỹ thuật này, chúng đều đã lỗi thời và không còn được sử dụng nữa.

Node.js hoạt động như thế nào

Cơ chế chung

Node.js sử dụng cơ chế non-blocking và event-driven để duy trì sự gọn nhẹ, hiệu quả trong các ứng dụng real time. Cần lưu ý rằng, Node.js chỉ cung cấp cho chúng ta thêm một lựa chọn nữa cho thế giới web, nó không phải là một công cụ để giải quyết mọi vấn đề.

Hiểu rõ điểm mạnh, điểm yếu của Node.js rất quan trọng. Ví dụ, sử dụng Node.js cho các ứng dụng nặng về tính toán là chúng ta đang tạo cơ hội để phát huy mọi sở đoản của nó. Node.js sẽ thích hợp nhất với các ứng dụng cần tốc độ, scale tốt và xử lý nhiều truy vấn đồng thời.

Cơ chế hoạt động của Node.js rất khác biệt. Với mô hình truyền thống, mỗi truy vấn sẽ được xử lý bởi một thread. Node.js chỉ sử dụng một thread duy nhất, với cơ chế non-blocking IO và event loop, cho phép xử lý lượng truy vấn đồng thời rất lớn (lên tới hàng triệu). Minh họa dưới đây mô tả trực quan hơn cơ chế hoạt động của Node.js khi so sánh với mô hình truyền thống.

diagram
Nguồn: soshace

Tính toán nhanh một chút, giả sử mỗi thread cần 2MB bộ nhớ thì một hệ thống có 8GB RAM (về lý thuyết) sẽ xử lý đồng thời được tối đa 4000 truy vấn. Với cơ chế khác, Node.js có thể xử lý đồng thời hàng triệu truy vấn, và có thể xử lý trên 600k truy vấn thông qua websocket.

Tất nhiên là cơ chế này có những hạn chế của riêng nó, khi chỉ có một thread duy nhất để thực thi chương trình. Ví dụ, với các ứng dụng yêu cầu tính toán nhiều, những yêu cầu này có thể gây quá tải thread và gây chậm trễ trong việc xử lý các truy vấn khác.

Ngoài ra, một điều bất tiện nữa là lập trình viên sẽ phải hết sức cẩn thận trong việc bắt và xử lý lỗi. Đặc biệt không được để lỗi xảy ra ở tầng cao nhất của event loop, nếu không có sẽ gây crash toàn bộ ứng dụng.

Một trong những kỹ thuật phổ biến để tránh crash chương trình đó là sử dụng callback để xử lý lỗi, thay vì để exception. Tuy nhiên, không phải lúc nào kỹ thuật này cũng có thể ứng dụng được. Một phương pháp khác đó là dùng các công cụ monitoring tiến trình Node.js và khi xảy ra crash sẽ tiến hành recover lại tiến trình đó (thông thường thì chỉ giúp duy trì chương trình hoạt động tiếp chứ khó mà lấy lại được dữ liệu cũ).

NPM – Node Package Manager

Sẽ thật là thiếu xót khi nói về Node.js mà không nhắc đến NPM, một trình quản lý package dành cho Node.js. Đây gần như là công cụ mặc định được cài đặt kèm với Node.js ở bất kỳ đâu. NPM được phát triển với tính năng tương tự Ruby Gems: Các package có sẵn, được lưu trữ online, có thể được tái sử dụng và cài đặt dễ dàng cùng với sự kiểm soát version và các package phụ thuộc khác.

Hiện nay, lượng package là rất lớn và vẫn đang tiếp tục tăng lên. Bất kỳ lập trình viên nào cũng có thể dễ dàng viết và công bố package của họ. Một số package phổ biến hiện nay có thể kể đến là:

  • express – Express.js hoặc Express, một framework phát triển web gọn nhẹ, gần như là lựa chọn số 1 hiện nay để phát triển các ứng dụng web với Node.js.
  • socket.iosockjs – Hai package phổ biến nhất hiện nay để làm việc với websocket.
  • pug – Template engine, lấy cảm ứng từ HAML, được dùng mặc định trong Express.js.
  • mongodbmongojs – Cung cấp API để làm việc với MongoDB trong Node.js.
  • redis – Làm việc với Redis.
  • lodash – Thư viện mở rộng tính năng cho JavaScript, rất tiện nếu làm việc với dữ liệu phức tạp.
  • moment – Thư viện để làm việc với kiểu dữ liệu ngày giờ rất tiện lợi và mạnh mẽ.

Một số ví dụ về việc dùng Node.js

Node.js có thể dùng lập trình rất nhiều ứng dụng khác nhau. Trong phần này tôi chỉ có thể trình bày một số ví dụ điển hình, phát huy ưu điểm của Node.js mà thôi. Có thể còn những trường hợp khác, nhưng hiểu biết của tôi về những ứng dụng này vẫn tương đối ít.

Chat

Ứng dụng chat có lẽ là ví dụ điển hình nhất về một ứng dụng real time, lưu lượng lớn, nhiều truy cập đồng thời. Các ứng dụng chat đã có nhiều thay đổi, từ IRC đến việc sử dụng các giao thức chuyên biệt và giờ đây, mọi thứ sẽ rất dễ dàng với Node.js và websocket chạy ngay trên cổng 80 chuẩn của HTTP.

Chat có lẽ là ví dụ tốt nhất để nói về ưu điểm của Node.js, cũng là ví dụ tốt cho những ai muốn làm ứng dụng đầu tiên với Node.js. Giả sử với tính năng đơn giản nhất, ứng dụng của chúng ta sẽ là một room chat, bất cứ ai cũng có thể tham gia và gửi tin nhắn cho tất cả mọi người.

Ở phía server, chúng ta có thể sử dụng Express.js và cài đặt hai thứ:

  • Một trang web bình thường với đường dẫn / hiển thị tin nhắn, ô nhập tin mới và nút gửi tin.
  • Websocket để nhận tin nhắn mới và sẽ gửi đến cho tất cả mọi người đang truy cập.

Ở phía client, chúng ta có hai việc cần xử lý: nhận tin nhắn mới và gửi lên server qua websocket, nhận tin nhắn của những người khác từ websocket và hiển thị.

Đây là một trường hợp đơn giản nhất của ứng dụng chat. Sẽ có nhiều điều cần làm hơn để nâng cao hiệu suất. Cache dữ liệu bằng Redis là một trong những cách rất hiệu quả. Hoặc một phương án tốt hơn, đó là sử dụng hàng đợi để xử lý tin nhắn đến ở phía server. Bằng cách này, chúng ta có thể xử lý lượng tin nhắn cực kỳ lớn, đồng thời nâng cao thêm một chút, chúng ta có thể lưu trữ tin nhắn cho người dùng offline và khi truy cập trở lại, họ có thể đọc các tin nhắn cũ.

Dù ứng dụng ở mức cơ bản hay nâng cao, thì đằng sau ứng dụng chat này, chúng ta sẽ chỉ cần đến Node.js với những nguyên tắc cơ bản của nó: event-driven, non-blocking IO, xử lý nhiều truy cập đồng thời.

API sử dụng NoSQL

Mặc dù Node.js rất thích hợp với những ứng dụng real time, không cần tính toán nhiều, điều đó không có nghĩa là nó không thích hợp với các loại ứng dụng khác. Đặc biệt với những ứng dụng cần thao tác với NoSQL (ví dụ MongoDB), Node.js thích hợp hơn các ngôn ngữ khác. Dữ liệu được lưu trữ trong những hệ cơ sở dữ liệu này tương tự như JSON, cho phép Node.js truy xuất dữ liệu rất dễ dàng mà không gặp vấn đề gì (vì không cần chuyển đổi kiểu dữ liệu).

Với những framework phát triển web khác, bạn có thể sẽ cần chuyển đổi một lần khi truy xuất dữ liệu từ MongoDB và chuyển đổi một lần nữa thành JSON khi trả kết quả về cho client. Với Node.js, mọi việc dễ dàng hơn rất nhiều, vì tất cả đều là JSON cả rồi. Nói ngắn gọn lại thì Node.js sẽ giúp chúng ta làm việc với các dữ liệu đồng nhất giữa server, client và database.

Ứng dụng xử lý bất đồng bộ nhiều

Nếu như một ứng dụng có lượng truy cập lớn, thì database thường sẽ trở thành điểm tắc cổ chai. Như chúng ta đã biết, Node.js có thể xử lý lượng truy cập đồng thời rất lớn, thế nhưng việc tắc cổ chai ở database lại là một vấn đề khác.

Nếu như database được truy cập đồng bộ (theo kiểu blocking), ứng dụng nào cũng dễ xảy ra lỗi khi truy cập tăng cao. Phương án đưa ra là cứ phản hổi người dùng trước đã, việc ghi dữ liệu vào database có thể đưa vào hàng đợi và xử lý từ từ.

Với cách làm này, thì ứng dụng có thể xử lý tốt lưu lượng rất lớn, tuy nhiên nó chỉ thích hợp với những ứng dụng mà người dùng không thực sự cần quan tâm đến việc dữ liệu đã thực sự được ghi hay chưa. Một số ví dụ điển hình là các hệ thống tracking, ghi log về hoạt động của người dùng, các hoạt động tương tác (như ấn Like chẳng hạn) không yêu cầu xử lý ngay lập tức (phía client thì có thể xử lý ngay cho người dùng biết thao tác của mình đã được ghi nhận).

Chúng ta có thể dùng một số hệ thống cache để làm hàng đợi, hoặc dùng các kiến trúc như RabbitMQ để làm hàng đợi và xử lý bất đồng bộ bằng một tiến trình khác, thậm chí là một server khác thích hợp để tính toán và xử lý các tác vụ nặng hơn.

message queuing
Nguồn: soshace

Giải pháp này phù hợp với mọi ngôn ngữ, framework chứ không riêng gì Node.js. Với Node.js, việc này khá dễ dàng nhờ cơ chế non-blocking và callback, chúng ta đơn giản là đẩy hết các công việc này vào một hàng đợi và gọi callback khi xử lý xong.

Stream

Trong hầu hết các ứng dụng web truyền thống, các truy vấn HTTP được xử lý hoàn toàn độc lập và tách biệt lẫn nhau. Node.js có thể mang đến ứng dụng web của chúng ta những tính năng rất ngầu dựa trên stream. Ví dụ, chúng ta upload một file rất lớn, thay vì chờ toàn bộ nội dung file được tải lên server, chúng ta có thể xử lý chúng ngay khi chúng đang được tải.

Cách làm là chúng ta sẽ dùng stream và đẩy file thông qua đó. Chúng ta sẽ xử lý dữ liệu nào được đưa lên server. Đây là một tính năng rất cần thiết với các ứng dụng real time mà làm việc với dữ liệu video hoặc âm thanh. Chiều ngược lại cũng như vậy, server có thể stream dữ liệu cho client từng phần một và client sẽ hiển thị lần lượt dữ liệu nhận được. Lưu ý rằng, chỉ có một số định dạng file mới cho phép stream như vậy, một số file có cấu trúc phức tạp cần phải có toàn bộ dữ liệu thì mới xử lý được.

Proxy

Với khả năng xử lý lượng truy vấn đồng thời rất lớn, Node.js rất thích hợp để xây dựng các proxy kết nối các dịch vụ khác nhau, hoặc dùng làm trung gian để kết nối đến các nguồn dữ liệu khác.

Giả sử chúng ta có một ứng dụng cần kết nối với các dịch vụ bên ngoài, lấy dữ liệu từ nhiều nguồn khác nhau, và lưu trữ các file hình ảnh lên một dịch vụ khác.

Nếu bạn cần một server làm trung gian thì Node.js có thể là một lựa chọn tốt. Ngay cả khi bạn đã có proxy rồi, thì Node.js vẫn là một lựa chọn tốt trong giai đoạn phát triển. Ví dụ, bạn có thể phát triển nhanh một server Node.js làm trung gian ở local, sau này khi đưa lên production, các biện pháp hiệu quả hơn (HAProxy, nginx, v.v…) có thể được nghĩ đến.

Monitoring dashboard

Khi bạn có một vài dịch vụ đang chạy, và bạn cần một trang web dạng dashboard để theo dõi những dịch vụ này, thì Node.js với cơ chế event loop, chúng ta có thể dễ dàng phát triển một dashboard như vậy. Tất nhiên hầu hết các xử lý sẽ là bất đồng bộ và websocket sẽ được sử dụng để cập nhật dữ liệu liên tục.

Bằng cách sử dụng Node.js và websocket, dashboard của chúng ta có thể nhận dữ liệu và cập nhật thông tin liên tục theo thời gian thực luôn. Những ứng dụng kiểu này rất đa dạng, như dashboard theo dõi hoạt động mạng, theo dõi trạng thái các server, lưu lượng người dùng, v.v…

Ứng dụng nào thì nên/không nên dùng Node.js

Các ứng dụng nên dùng Node.js

Node.js cùng với Express.js có thể tạo nên các trang web theo mô hình truyền thống. Tuy nhiên, thực sự thì việc truy vấn HTML không phải cách mà chúng ta nên dùng với Node.js. Cách làm tốt hơn cả là nên dùng một framework khác cho client (ví dụ React, Vue) và việc giao tiếp với server chỉ nên thông qua API mà thôi.

Ngoài ra, như đã nói ở trên, nếu ứng dụng của bạn là real time, không cần tính toán quá nhiều và cần xử lý lượng truy vấn rất lớn thì Node.js thích hợp hơn cả. Hơn nữa, nếu ở tầng database, bạn lưu trữ dạng object (ví dụ NoSQL như MongoDB) thì Node.js sẽ là lựa chọn cực kỳ đúng đắn.

Ứng dụng nào không nên dùng Node.js

Nếu ứng dụng của bạn cần tính toán nhiều, Node.js không phải là một lựa chọn tốt. Bạn vẫn có thể dùng Node.js nhưng các ngôn ngữ lập trình khác chạy multi thread sẽ cho hiệu suất tốt hơn. Việc chỉ dùng một thread khiến Node.js khó có thể so sánh với các ngôn ngữ khác.

Để giải quyết vấn đề này, Node.js đưa ra một giải pháp là cluster module. Chúng ta có thể khởi tạo nhiều tiến trình Node.js cùng lúc thông qua proxy như nginx. Tuy nhiên, ngay cả với cluster, các tác vụ nặng về tính toán cũng không nên dùng Node.js.

Cách làm được khuyến cáo là Node.js sẽ đưa những tác vụ này vào hàng đợi, sau đó chúng sẽ được xử lý bất đồng bộ. Lúc này chúng ta có thể lựa chọn những ngôn ngữ khác, những công cụ với hiệu suất tốt hơn để xử lý các tác vụ này. Đương nhiên là chúng ta cần đến RabbitMQ hoặc các dịch vụ khác để làm hàng đợi.

Với cách làm này, ứng dụng của chúng ta sẽ xử lý các tác vụ rất tốt, và có thể scale dễ dàng. Về mặt logic, Node.js và các tác vụ nặng được xử lý hoàn toàn độc lập, và có thể đưa sang các server với CPU, RAM thích hợp hơn.

Cách làm này thì không phải chỉ Node.js mới làm được mà thực ra bất kỳ ngôn ngữ nào cũng làm được. Nhưng Node.js vẫn rất thích hợp để xử lý bước đầu các truy vấn người dùng và đẩy tác vụ nặng vào hàng đợi, vì như chúng ta đã biết, xử lý bước đầu rất đơn giản và Node.js có thể xử lý hàng triệu truy vấn như vậy.

Kết luận

Bài viết trình bày những hiểu biết của tôi về Node.js, các trường hợp nào nên dùng và không nên dùng. Như tôi đã nói, Node.js không thể giải quyết mọi bài toán, nó không phải là công cụ để thay thế các công cụ khác, mà nó cho chúng ta thêm một lựa chọn.

Tôi xin lỗi nếu bài viết có bất kỳ typo nào. Nếu bạn nhận thấy điều gì bất thường, xin hãy cho tôi biết.

Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.