- vừa được xem lúc

Những cách mình làm để scale NodeJS Application

0 0 4

Người đăng: Cao Chi Hai

Theo Viblo Asia

Xin chào, lại là mình, một NodeJS developer đã đi làm được một thời gian. Thực ra mình cũng chưa phải đối mặt với những con số traffic khổng lồ như 1k, 10k hay 1M user đâu, nhưng trong quá trình làm việc, mình đã cố gắng học hỏi và áp dụng các best practice để code clean, hiệu quả, scalable và high performance.

Trước khi đi vào chi tiết các kỹ thuật scaling, mình ôn lại một số đặc điểm quan trọng của NodeJS để hiểu tại sao việc scaling NodeJS lại có những đặc thù riêng.

NodeJS là gì? Có những đặc điểm quan trọng nào?

NodeJS là một JavaScript runtime được xây dựng trên V8 engine của Chrome, cho phép chạy JavaScript ở ngoài browser. Các đặc điểm quan trọng của NodeJS mà mình có thể thấy trực tiếp trên document của NodeJS, bao gồm: event-driven, asynchronous, non-blocking I/O, single thread.

1. Event-Driven

NodeJS được thiết kế theo kiến trúc event-driven, mọi thứ đều xoay quanh các events. Khi có một event xảy ra (như request HTTP, file được đọc xong, database trả về kết quả, EventEmitter, …), NodeJS sẽ gọi các callback, Promise tương ứng.

2. Asynchronous

Hầu hết các API trong NodeJS đều hoạt động theo cơ chế asynchronous. Không cần chờ đợi một tác vụ hoàn thành mới có thể thực hiện tác vụ tiếp theo. Thay vào đó, bạn có thể khởi động nhiều tác vụ cùng lúc và xử lý kết quả khi chúng sẵn sàng. Ví dụ như fs.readFile(), fs.writeFile(), http.get(), eventEmitter.on(), …

3. Non-blocking I/O

Đây là điểm mạnh lớn nhất của NodeJS. Thay vì chờ đợi các tác vụ I/O (đọc file, query database, gọi API) hoàn thành rồi mới làm việc khác, NodeJS sẽ delegate cho libuv (Thread pool và Event Loop), sau đó, tiếp tục xử lý các tác vụ khác. Khi các tác vụ I/O hoàn thành, libuv sẽ thông báo cho NodeJS biết để gọi các callback, Promise tương ứng.

4. Single Thread

NodeJS hoạt động trên một thead duy nhất (main thread) nhưng điều này không có nghĩa là NodeJS chỉ có thể xử lý một việc tại một thời điểm. Main thread chịu trách nhiệm quản lý các event và callback, không phải chờ đợi các tác vụ I/O hoàn thành. Thread pool và Event Loop chịu trách nhiệm xử lý các tác vụ I/O. Nhờ đó mà NodeJS có thể xử lý nhiều request đồng thời mà không cần tạo thread mới cho mỗi request.

5 Cách Scale Node.js Application

1. Load Balancing với Nginx

Khi NodeJS application bắt đầu nhận nhiều request hơn, việc chạy chỉ một instance có thể không đủ. Đây là lúc load balancer trở nên cần thiết, và mình dùng Nginx để làm load balancer.

Tại sao cần Load Balancer?

  • Phân phối traffic đều cho nhiều instance
  • Tăng độ tin cậy - nếu một instance crash, các instance khác vẫn hoạt động
  • Có thể scale horizontal dễ dàng

Cách setup Nginx làm load balancer:

upstream nodejs_backend { ip_hash; server 127.0.0.1:3000 weight=3 max_fails=3 fail_timeout=30s; server 127.0.0.1:3001 weight=2 max_fails=3 fail_timeout=30s; server 127.0.0.1:3002 weight=1 max_fails=3 fail_timeout=30s;
} server { listen 80; server_name example.com; location / { proxy_pass http://nodejs_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; }
}

2. Sử dụng worker_threads để tối ưu

Vì NodeJS là single thread, nên chỉ dùng được 1 core CPU. Trong khi đó, thực tế server có thể có 2, 4, 8, 16 core. Để tận dụng được tối đa core CPU của server, mình sử dụng các built-in module là worker_threads.

worker_threads cho phép NodeJS chạy song song trong các threads riêng biệt, chia sẻ memory trong cùng một process. Giữ các main thread với worker thread và giữa các worker thread có thể giao tiếp với nhau. Vì main thread và worker threads là riêng biệt nên OS sẽ phân bổ worker thread ở các core khác, giúp tận dụng tối đa core CPU.

Case study thực tế mình đã áp dụng worker_threads trong hệ thống risk managment. Mình chia hệ thống thành 2 phần là:

Main Thread có nhiệm vụ quan trọng và cấp bách nhất:

  • Theo dõi danh mục đầu tư (portfolio) của người dùng.
  • Thực hiện lệnh force close ngay lập tức nếu khoản lỗ đạt ngưỡng.

Worker Threads được dùng để xử lý các tác vụ nặng, tiêu tốn nhiều tài nguyên CPU:

  • Nhận và xử lý dữ liệu thị trường real-time từ Binance.
  • Cập nhật trạng thái các lệnh giao dịch.

Main thread luôn "rảnh rỗi" để quản lí rủi ro, các worker threads xử lý dữ liệu nền mà không làm nghẽn hệ thống. Cách tiếp cận này giúp tận dụng tối đa các core CPU của server, đảm bảo tính ổn định và tốc độ phản hồi của hệ thống.

Ngoài worker_threads, thì NodeJS cũng còn 2 module built-in khác có thể sử dụng để tối ưu core CPU là clusterchild_process

3. Tách services theo role

Thực tế, mình thường phân chia các instance của NodeJS application theo các role riêng biệt. Các role-based instance này có thể chạy độ lập, chịu trách nhiệm khác nhau, scale độc lập và deploy riêng. Mình sẽ sử dụng REST, gRPC, Message Queue (BullMQ, Redis Pub/Sub) để giao tiếp giữa các instances.

Một vài lợi ích có thể kể đến:

  • Module hóa được codebase, dễ quản lí hơn.
  • Có thể scale các instances đang bị tải cao dễ dàng.
  • Có thể deploy riêng theo role

Các role trên thực tế mình hay định nghĩa gồm:

  • api: chuyên để xử lí request của user
  • worker: chuyên để xử lí các tác vụ nặng về I/O (ví dụ như tính toán nặng, xử lí dữ liệu thị trường, xử lí file lớn, …)
  • load_balancer: chuyên để xử lí điều hướng traffic
  • publisher/subscriber: publisher dùng để produce các task, còn subscriber sẽ consume. Thường role “api” sẽ là publisher, role “worker” sẽ là consumer. Đôi khi, publisher và consumer là các instance riêng lẻ.

4. Tối ưu I/O bằng Worker instance

Với các tác vụ tốn thời gian như tính toán phức tạp, xử lí file lớn, xử lí database, làm báo cáo. Các tác vụ này sẽ được đẩy vào Queue (BullMQ) để xử lý, và một Worker instance sẽ có nhiệm vụ xử lý chúng ở background.

Worker instance này thường sẽ là một server có cấu hình tương tự hoặc cao hơn, nhiều tài nguyên để xử lí các tác vụ nặng. Đồng thời, việc tách riêng Worker ra một server riêng còn giảm thiểu rủi ro khi Worker xử lí nặng làm ảnh hưởng đến các instance khác.

Các lợi ích có thể kể đến:

  • Giảm thời gian phản hồi cho user
  • Chạy xử lý song song ở background
  • Scale worker theo tải hệ thống

5. Sử dụng Redis để cache và tận dụng các data structure

Caching là một các phổ biến và hiệu quả để cải thiện performance của không chỉ NodeJS application.

Một số loại cache mình đã xử dụng thực tế như cache lại query từ database, cache response api, cache token, session.

Ngoài ra, mình cũng sử dụng Sorted Set data structure của Redis. Đây là case study thực tế mà mình đã áp dụng. Trong một ứng dụng trading, mình cần track position của user và tính toán mức giá mà tại đó user sẽ bị lỗ 5%. Thay vì query database và sort mỗi lần, mình sử dụng Redis Sorted Set.

Vấn đề:

  • Có nhiều user với nhiều position khác nhau
  • Cần nhanh chóng tìm ra những user có nguy cơ lỗ >= 5% tại mức giá hiện tại
  • Data thay đổi liên tục theo real-time price

Tại sao Sorted Set hiệu quả:

  • Tự động sort: Không cần sort lại mỗi lần query
  • Unique members: Mỗi user chỉ có một record, tự động update
  • Range queries: Có thể query theo range price rất nhanh O(log(N)) (binary search)
  • Memory efficient: Chỉ lưu userId và score, metadata riêng biệt

Lời kết

Đây là 5 cách mà mình đã học được và áp dụng để scale cho phần lớn Node.js applications. Các ý chính mình nghĩ là cần nắm bắt là:

  • Load balancer để chia tải
  • Tận dụng CPU core
  • Phân tách services
  • Queue/Worker để xử lý async
  • Redis để tăng tốc độ

Hi vọng sẽ hữu ích với mọi người ^_^

Bình luận

Bài viết tương tự

- vừa được xem lúc

Tính toán bất đồng bộ quy mô lớn ở Facebook

Chúng ta lên Face mỗi ngày, tuy nhiên không phải ai cũng chú ý tới rằng Facebook xử lý các tương tác của chúng ta như thế nào đúng không ^^ Trên thực tế, hệ thống của Facebook phải xử lý hàng tỷ reque

0 0 76

- vừa được xem lúc

Scale Database với kiến trúc Master Slave

Sau bài viết đầu tiên về đầu tiên những sai lầm khi làm việc với CSDL mình đã nhận được rất nhiều chia sẻ tích cực từ. Các anh chị bạn bè cũng đã có những góp ý về cách viết, nội dung, cách sắp xếp cá

0 0 37

- vừa được xem lúc

Laravel có thể scale không? | Does Laravel Scale?

Trong bài viết này, tôi sẽ khám phá xem liệu bạn có thể sử dụng Laravel ở quy mô lớn và liệu nó có thể được sử dụng để vận hành những ứng dụng lớn như Twitter, Facebook hoặc các ứng dụng khác không. C

0 0 29

- vừa được xem lúc

STATELESS ARCHITECTURE

Trong kiến trúc Stateless, các HTTP request từ client có thể được gửi đến bất kỳ Web Server nào trong cụm gồm nhiều Web Server, để lấy state data từ một Shared Storage. Đây chính là ví dụ về một hệ th

0 0 29

- vừa được xem lúc

DATABASE SCALING - KỸ THUẬT THƯỜNG XUYÊN ĐƯỢC TRIỂN KHAI TRONG CÁC DỰ ÁN LỚN

Khi số lượng người dùng ứng dụng của bạn ngày càng tăng lên, dữ liệu từ đó sẽ tăng trưởng ngày càng nhiều hơn mỗi ngày, database của dự án sẽ dần trở nên quá tải. Và đây chính là lúc chúng ta cần thực

0 0 31

- vừa được xem lúc

Hành Trình Phát Triển Hạ Tầng Kỹ Thuật của Facebook: Từ Khởi Nguồn Đến Hệ Thống Phân Tán Toàn Cầu

Bài viết này khám phá hành trình phát triển hạ tầng kỹ thuật của Facebook từ những ngày đầu khi còn là một hệ thống tập trung đơn giản, đến khi trở thành một nền tảng phân tán toàn cầu với hệ thống No

0 0 28