🎉 Giảm 20% cho đơn hàng đầu tiên!✦🚚 Miễn phí vận chuyển cho đơn trên 500.000₫✦⚡ Flash Sale — Số lượng có hạn!✦🎁 Quà tặng kèm cho đơn từ 1.000.000₫✦🎉 Giảm 20% cho đơn hàng đầu tiên!✦🚚 Miễn phí vận chuyển cho đơn trên 500.000₫✦⚡ Flash Sale — Số lượng có hạn!✦🎁 Quà tặng kèm cho đơn từ 1.000.000₫✦
Server 20 USD gánh hơn 46.000 user không cần cache: nút thắt nằm ở database, không phải framework
Quay lại Blog

Server 20 USD gánh hơn 46.000 user không cần cache: nút thắt nằm ở database, không phải framework

2 tháng 11, 20265 phút đọcVũ Khôi
Tin tức
#Wotech
Vũ Khôi

Vũ Khôi

Tác giả tại Wotech Electronics Blog

Server 20 USD gánh hơn 46.000 user không cần cache: nút thắt nằm ở database, không phải framework

Một benchmark cộng đồng đang được giới lập trình viên chia sẻ rộng rãi: cùng một ứng dụng, cùng một VPS giá khoảng 20 USD (8 vCPU, 16GB RAM, chạy chung cả ứng dụng lẫn PostgreSQL), kết quả chịu tải dao động từ 8.300 đến hơn 46.000 người dùng ảo — tương đương tới khoảng 11.500 request/giây. Khác biệt không đến từ việc chọn ngôn ngữ "nhanh" hay "chậm", mà từ cách ứng dụng làm việc với database.

Điều kiện benchmark khắt khe đến mức nào?

Ứng dụng dùng để đo là Conduit — bản clone của Medium với 19 REST API endpoints. Điều kiện đo được đặt ra khá sát thực tế và không hề dễ:

  • Một server duy nhất: 8 dedicated vCPU, 16GB RAM, ứng dụng và PostgreSQL chạy chung (không tách DB server riêng).
  • Dữ liệu sẵn có: 200.000 users, 500.000 bài viết, 5 triệu lượt favorite, 2,5 triệu lượt follow.
  • Cấm mọi hình thức caching: không Redis, không cache response trong bộ nhớ. Mọi request đều phải đọc/ghi trực tiếp xuống PostgreSQL.
  • Mọi lượt đăng nhập đều phải verify mật khẩu bằng bcrypt cost 10; endpoint ghi phải commit xong mới trả response.
  • Tiêu chuẩn đạt: độ trễ P95 dưới 500ms, P99 dưới 1.000ms và tỉ lệ lỗi dưới 1% trong suốt 5 phút chịu tải liên tục.

Bài đo khởi đầu bởi tác giả Arjay McCandless trên các ngôn ngữ Go, Rust, Java và Python cho kết quả 10.000–24.000 users. Sau đó toàn bộ được mở mã nguồn để cộng đồng tự tối ưu bằng các framework quen thuộc trong thực tế — và những bài học đáng giá nhất nằm ở các bài nộp sau đó.

Bài học 1: Điểm nghẽn thực sự thường nằm ở database

Bài nộp C# dùng ASP.NET Core Minimal APIs với Entity Framework Core chỉ đạt 8.300 users. Nhìn vào biểu đồ tài nguyên mới thấy vấn đề: ứng dụng C# chỉ chiếm 19% CPU, trong khi PostgreSQL nuốt trọn 78% CPU của cả máy. Code ứng dụng chạy rất nhanh, nhưng các truy vấn do ORM sinh ra quá nặng khiến database trở thành nút thắt cổ chai.

Ruby on Rails (chạy Falcon server, bật YJIT) đạt 10.000 users (khoảng 2.600 RPS) với CPU ứng dụng 59% và DB 30% — cũng là một bức tranh tương tự: framework không phải phần chậm nhất của hệ thống.

Bài học 2: Tách query phân trang mang lại hiệu năng đột biến

Bài thi Rust (Axum + Tokio + Tokio-Postgres) đạt 43.000 users (khoảng 11.000 RPS). Bí quyết lớn nhất nằm ở logic lấy danh sách bài viết: thay vì JOIN hàng loạt bảng ngay từ đầu, tác giả chỉ SELECT lấy danh sách ID của trang hiện tại trước, sau đó mới tính tags, số favorite và trạng thái cho riêng các ID đó. Tránh tính toán dư thừa cho những dòng dữ liệu sẽ bị bỏ qua giúp tiết kiệm một lượng lớn CPU cho database.

Bài học 3: Bỏ ORM hydration, để PostgreSQL tự build JSON

Bước nhảy vọt ấn tượng nhất đến từ Ruby và Python khi rời framework truyền thống sang stack ở tầng thấp:

  • Bản Ruby không dùng Rails (chỉ Rack trên Falcon, bỏ ActiveRecord): từ 10.000 vọt lên 37.000 users.
  • Bản Python không dùng web framework nào (uvloop + httptools + asyncpg + orjson): từ 10.000 lên 46.000 users — khoảng 11.500 RPS, cao nhất bảng.

Cả hai cùng một tư duy: bỏ bước ánh xạ dòng dữ liệu từ DB sang object của ngôn ngữ lập trình (ORM hydration) rồi lại serialize object thành JSON. Thay vào đó, họ để PostgreSQL trực tiếp tổng hợp chuỗi JSON bằng json_build_object và stream thẳng ra response, kết hợp query pipelining để giảm số vòng giao tiếp mạng (round-trips) giữa ứng dụng và database.

Những kết quả đáng chú ý khác

  • Kotlin (Ktor + Vert.x Postgres client): 44.000 users — nhờ tận dụng triệt để Coroutines và query pipelining qua Vert.x client.
  • Lua (OpenResty / Nginx + LuaJIT): 33.000 users — tách việc verify bcrypt sang pool luồng riêng để không chặn event loop của Nginx.
  • PHP (Symfony micro-kernel + Swoole): 32.000 users với P95 chỉ 234ms — nhờ Swoole giữ worker chạy nền liên tục thay vì khởi động lại framework theo từng request.
  • Elixir (Phoenix + Ecto): 19.000 users — chạm trần khi connection pool bắt đầu loại bỏ các request chờ quá lâu.

Bài học rút ra cho kiến trúc hệ thống

Tối ưu framework hay runtime chỉ giải quyết một phần câu chuyện. Khi ứng dụng và database cùng chia sẻ tài nguyên, ba việc nên ưu tiên rà soát trước tiên là:

  1. Giảm lượng công việc database phải làm trên từng request — tối ưu câu SQL, tách bước lấy ID trước khi JOIN dữ liệu phụ.
  2. Cắt giảm các bước serialize/deserialize trung gian giữa tầng database và HTTP response.
  3. Cấu hình connection pool hợp lý, tách riêng pool đọc/ghi để tránh nghẽn đầu hàng (head-of-line blocking) khi xuất hiện các request ghi nặng.

Với các doanh nghiệp nhỏ đang chạy hệ thống trên một VPS khiêm tốn, đây là tin tốt: trước khi nghĩ đến chuyện nâng cấp server hay thêm Redis, hãy rà soát lại cách ứng dụng truy vấn database. Rất có thể chiếc server 20 USD hiện tại vẫn còn gánh được gấp nhiều lần lượng người dùng hiện nay — nếu được đối xử đúng cách.

Nguồn: bài chia sẻ benchmark Conduit của cộng đồng lập trình viên (bài đo gốc của Arjay McCandless, mở mã nguồn cho cộng đồng tối ưu), tháng 11/2026.