Trong thử thách tối ưu hóa hiệu năng trên một chiếc Virtual Machine giá 12 USD (1 shared vCPU, 2GB RAM, 50GB lưu trữ), cộng đồng lập trình viên đã gửi về 28 bản cài đặt bằng 13 ngôn ngữ khác nhau cho một hệ thống Social Media API sử dụng SQLite chạy in-process. Kết quả kỷ lục đã nâng mức chịu tải từ 14.050 lên đến 64.000 concurrent users (chạm trần giới hạn Connection Tracking Table của Linux kernel). Hai ngôn ngữ dẫn đầu là Rust và C++, trong đó Rust chiến thắng chung cuộc nhờ p99 latency chỉ 224ms và mức tiêu thụ tài nguyên cực thấp (52% CPU, dưới 100MB RAM). Các kỹ thuật cốt lõi tạo nên sự đột phá bao gồm: loại bỏ hoàn toàn Nginx và các tầng trung gian của Web Framework, sử dụng Group Commit cho Database, đẩy việc serialize JSON xuống thẳng SQLite, quản lý bộ nhớ bằng Memory Pool/Stack Allocation và can thiệp sâu vào nhân Linux với giao thức io_uring. Đáng chú ý là thử thách lần này hoàn toàn vắng bóng các bài dự thi từ Java và Node.js.
============================
Chào mọi người, hôm nay mình vừa xem được một video cực kỳ cuốn từ kênh của Arjay McCandless về chủ đề System Design và Performance Optimization, nên muốn tóm tắt và chia sẻ lại cho anh chị em cùng đọc và bàn luận.
Câu chuyện bắt đầu từ tuần trước, khi tác giả thử nghiệm benchmark 8 ngôn ngữ lập trình trên một con server giá rẻ 12 đô mỗi tháng (cấu hình vỏn vẹn 1 shared vCPU, 2GB RAM và 50GB ổ cứng) với cấu hình mặc định gồm Framework phổ biến nhất, PostgreSQL và Nginx. Khi đó, kết quả tốt nhất thuộc về Rust kết hợp với SQLite, chịu được khoảng 14.050 concurrent users. Rất nhiều anh chị em developer trên mạng đã cho rằng họ có thể tối ưu tốt hơn nhiều. Thế là tác giả mở luôn một GitHub repository, đưa ra đề bài rõ ràng và thách thức cộng đồng cùng vào tối ưu.
Luật chơi và tiêu chuẩn của bài Benchmark
Để đảm bảo tính công bằng, mọi người tham gia đều phải xây dựng chung một hệ thống Social Media API đơn giản (giống Twitter) với 5 endpoints: health check, load feed, mở bài viết, like bài viết và tạo bài viết mới. Các ràng buộc kỹ thuật rất chặt chẽ:
Tổng cộng có 28 bài nộp từ 13 ngôn ngữ, và 26 bài hợp lệ được đưa vào chạy benchmark (1 bài C++ bị loại vì nạp toàn bộ dữ liệu vào in-memory database sai luật, 1 bài R không chạy được do thiếu thư viện hệ thống). Một điều khá bất ngờ mà tác giả có nhắc khéo là không có bất kỳ bài nộp nào đến từ đội Java và Node.js.
Nhìn lại toàn bộ các bản cài đặt đạt điểm cao nhất, bạn sẽ thấy có 5 mẫu số chung luôn lặp đi lặp lại:
-------------
Anh chị em thấy ấn tượng với màn trình diễn của ngôn ngữ nào nhất trong đợt benchmark này? Và nếu viết bằng Java hay Spring Boot với Virtual Threads hoặc Netty thuần thì liệu sẽ trụ được ở mốc bao nhiêu trên con server 1 core 2GB RAM này? Cùng để lại bình luận trao đổi nhé!







============================
Chào mọi người, hôm nay mình vừa xem được một video cực kỳ cuốn từ kênh của Arjay McCandless về chủ đề System Design và Performance Optimization, nên muốn tóm tắt và chia sẻ lại cho anh chị em cùng đọc và bàn luận.
Câu chuyện bắt đầu từ tuần trước, khi tác giả thử nghiệm benchmark 8 ngôn ngữ lập trình trên một con server giá rẻ 12 đô mỗi tháng (cấu hình vỏn vẹn 1 shared vCPU, 2GB RAM và 50GB ổ cứng) với cấu hình mặc định gồm Framework phổ biến nhất, PostgreSQL và Nginx. Khi đó, kết quả tốt nhất thuộc về Rust kết hợp với SQLite, chịu được khoảng 14.050 concurrent users. Rất nhiều anh chị em developer trên mạng đã cho rằng họ có thể tối ưu tốt hơn nhiều. Thế là tác giả mở luôn một GitHub repository, đưa ra đề bài rõ ràng và thách thức cộng đồng cùng vào tối ưu.
Luật chơi và tiêu chuẩn của bài Benchmark
Để đảm bảo tính công bằng, mọi người tham gia đều phải xây dựng chung một hệ thống Social Media API đơn giản (giống Twitter) với 5 endpoints: health check, load feed, mở bài viết, like bài viết và tạo bài viết mới. Các ràng buộc kỹ thuật rất chặt chẽ:
- Phải vượt qua bộ test suite gồm 42 bài kiểm tra và trả về format response khớp tuyệt đối với đặc tả.
- Bắt buộc dùng SQLite chạy chung process với ứng dụng. Không được dùng Database server riêng biệt, không PostgreSQL, không Redis hay bất kỳ Caching server bên ngoài nào. Không được tự lưu dữ liệu vào cấu trúc in-memory riêng để lách luật.
- Mọi request có xác thực đều phải verify chữ ký của login token.
- Khi tạo bài viết hoặc bấm like, transaction bắt buộc phải commit thành công xuống database trước khi trả kết quả về cho user. Anh chị em được phép gom nhiều lệnh ghi vào một transaction (batch writes), nhưng tuyệt đối không được trả response trước khi transaction đó thực sự commit.
- Công cụ tạo tải là k6, giả lập hành vi người dùng thực tế (tải feed, đợi vài giây, đọc bài, thỉnh thoảng bấm like hoặc đăng bài).
- Tiêu chí vượt qua mỗi cấp độ tải: Server phải duy trì mức tải đó liên tục trong 5 phút với p95 latency dưới 500ms, p99 latency dưới 1 giây, và tỷ lệ lỗi (error rate) dưới 1%.
Tổng cộng có 28 bài nộp từ 13 ngôn ngữ, và 26 bài hợp lệ được đưa vào chạy benchmark (1 bài C++ bị loại vì nạp toàn bộ dữ liệu vào in-memory database sai luật, 1 bài R không chạy được do thiếu thư viện hệ thống). Một điều khá bất ngờ mà tác giả có nhắc khéo là không có bất kỳ bài nộp nào đến từ đội Java và Node.js.
Chi tiết kết quả và cách tối ưu qua từng ngôn ngữ
Dưới đây là hành trình đi từ mức tải thấp nhất lên đến đỉnh điểm của bảng xếp hạng:- Ruby (14.000 concurrent users):
Hai bản dùng Ruby on Rails tiêu chuẩn chỉ chịu được khoảng 3.000 đến 3.500 users. Tuy nhiên, bản Ruby nhanh nhất đã vọt lên 14.000 users (gấp 4 lần Rails) với p95 là 50ms, p99 là 154ms, 0% lỗi và CPU mới dùng 62% (nhưng lên 15.000 users thì p99 vọt lên 2.5 giây). Bí quyết ở đây là tác giả bản này dùng web server Iodine (viết bằng C) chạy single worker, single thread, bật trình biên dịch YJIT của Ruby. Quan trọng hơn cả là cách xử lý JSON: thay vì lấy các dòng dữ liệu từ database, khởi tạo Ruby object rồi mới chạy qua bộ JSON serializer rất tốn kém, lập trình viên đã để SQLite tự build chuỗi JSON trực tiếp và Ruby chỉ việc chuyển tiếp chuỗi đó về cho client. - Erlang (15.000 concurrent users):
Sử dụng web server Cowboy và tận dụng cơ chế lightweight processes đặc trưng của Erlang (mỗi kết nối một process riêng). Vì chạy trên máy chỉ có 1 vCPU, lập trình viên đã cấu hình runtime về một scheduler duy nhất và chỉ định một process độc quyền giữ kết nối SQLite, đồng thời cũng tạo JSON ngay trong SQLite. Ở mốc 15.000 users, p95 đạt 171ms, p99 đạt 320ms, tiêu thụ 600MB RAM và 83% CPU, trước khi bị cạn kiệt CPU ở mốc 16.000 users. - Python (22.000 concurrent users):
Ở video trước, FastAPI kết hợp PostgreSQL chỉ đạt 2.150 users. Lần này, một bản FastAPI tiêu chuẩn đi cùng SQLite, uvloop và orjson đạt 11.000 users. Sau đó, một developer khác đã nhân đôi con số này lên 22.000 users (p95 đạt 353ms, p99 đạt 653ms, CPU 91%). Khi chạy profiling, người này phát hiện phần lớn thời gian CPU bị tiêu tốn bên trong tầng FastAPI và Starlette chỉ để luân chuyển request qua Dependency Injection, Middleware và khởi tạo Request/Response objects. Giải pháp là gỡ bỏ toàn bộ các lớp vỏ framework đó, cho phép Uvicorn gọi thẳng vào hàm handler xử lý chính. - Go (22.000 concurrent users):
Chỉ có duy nhất 1 bài nộp bằng Go và đồng hạng với Python. Điểm sáng của bản cài đặt này là kỹ thuật Group Commit: thay vì để mỗi HTTP request tự gọi xuống SQLite, mọi thao tác được đẩy qua một goroutine duy nhất quản lý database. Goroutine này gom tối đa 256 requests mỗi lần, thực thi và commit tất cả lệnh ghi trong một transaction duy nhất rồi mới trả kết quả về cho từng client. Đáng tiếc là bản Go này tự giới hạn khi vượt quá 16.000 kết nối mở thì bắt đầu yêu cầu client disconnect và reconnect liên tục. Chi phí bắt tay lại ở quy mô lớn khiến tỷ lệ lỗi vượt quá 1% ở mốc 23.000 users dù CPU mới chỉ dùng hết 78% và p95 ở mốc 22.000 chỉ có 143ms. - C# (32.000 concurrent users):
Tăng vọt từ mức 4.400 users của video trước. Bản cài đặt này giữ lại web server Kestrel của ASP.NET Core nhưng loại bỏ hoàn toàn Middleware, Routing layer và các đối tượng HttpContext thông thường để giao tiếp trực tiếp với các low-level interfaces của Kestrel. Để tránh tạo gánh nặng cho bộ dọn rác (Garbage Collector) trên máy 1 core, developer đã dùng các pooled buffers để tạo JSON và xác thực token thông qua vùng nhớ cấp phát trực tiếp trên Stack (stack-allocated memory). Hệ thống đạt 32.000 users với p95 346ms, p99 731ms và vắt kiệt tới 93% CPU. - Fortran (38.000 concurrent users):
Một sự xuất hiện không ai ngờ tới từ ngôn ngữ ra đời từ những năm 1950 nhưng lại đánh bại cả Python, Go và C#. Bản này dùng thư viện C tên là libmicrohttpd để nhận kết nối HTTP rồi nối vào code Fortran. Do không dùng thư viện JSON trong Fortran, tác giả đẩy toàn bộ việc parse/build JSON và cả việc kiểm tra chữ ký login token vào thẳng bên trong câu truy vấn SQL của SQLite, kết hợp với Group Commit. Ở mốc 38.000 users, p99 là 944ms, CPU 85% và dùng 700MB RAM. - PHP (40.000 concurrent users):
Trong thử nghiệm ban đầu, Laravel chỉ chịu được 750 users (và PHP-FPM thuần đạt 2.700 users) do chi phí khởi tạo lại toàn bộ framework ở mỗi request. Bản nộp lần này chỉ vỏn vẹn 250 dòng code nhưng chịu tải tới 40.000 users (gấp hơn 50 lần Laravel) với p95 chỉ 189ms và p99 là 853ms. Vũ khí bí mật là Swoole (một C extension giúp chạy PHP như một long-running server, chỉ khởi tạo ứng dụng, kết nối DB và prepare SQL queries đúng một lần duy nhất) kết hợp với việc bật trình biên dịch JIT của PHP. Một bản Swoole thứ hai cũng đạt tới 39.000 users nhờ patch lại Swoole để giảm RAM cho các kết nối nhàn rỗi. - C (46.000 concurrent users):
Sử dụng giao diện bất đồng bộ io_uring của Linux kernel để gửi và nhận các system calls theo lô (batches), giảm thiểu chi phí chuyển đổi ngữ cảnh (context switch) giữa user space và kernel space. Đặc biệt hơn, lập trình viên này đã áp dụng kỹ thuật Profile-Guided Optimization (PGO) ngay trong lúc biên dịch mã nguồn của SQLite: chạy thử tải thực tế để xem nhánh code nào của SQLite được gọi nhiều nhất, rồi biên dịch ra một bản binary SQLite tối ưu riêng cho đúng các câu query của bài test, đồng thời viết lại câu query lấy bài viết trên feed bằng một lần quét range scan duy nhất thay vì đếm like lặp đi lặp lại. Bản C này ăn cực kỳ ít tài nguyên: chỉ 39MB RAM (thấp nhất toàn giải) và 36% CPU ở mốc 46.000 users (p95 54ms, p99 178ms). Tuy nhiên ở mốc 47.000 users thì p99 bị lố 43ms (1.043ms) do thiết kế dùng 8 worker threads cùng tranh chấp 1 lock duy nhất để vào database và phải gom đủ 128 events mới xử lý một lần. - TypeScript trên Bun (47.000 concurrent users):
Từ mức 4.200 users ở video trước, bản cài đặt dài 350 dòng code này tăng gấp 11 lần lên 47.000 users (p95 96ms, p99 356ms, 72% CPU và 150MB RAM). Thay vì dùng HTTP server sẵn có của Bun, tác giả làm việc trực tiếp với raw TCP sockets và tự viết bộ parser chỉ quét đúng những byte header mà API này cần. Chuỗi JSON được SQLite tạo ra và trả về dưới dạng raw bytes, đẩy thẳng vào một output buffer tái sử dụng để gửi về client mà không cần ép kiểu sang chuỗi String của JavaScript, đồng thời gọi hàm C để tăng giới hạn connection backlog. - Zig (61.000 concurrent users):
Chỉ với khoảng 680 dòng code, sử dụng thư viện mạng uSockets (viết bằng C) và Group Commit, Zig đạt tới 61.000 users (p99 chỉ 268ms, CPU 69%). Lý do duy nhất khiến Zig dừng lại ở con số này là vì lập trình viên đã hardcode giới hạn 60.000 persistent connections trong code (cùng một tác giả này cũng nộp 2 bản C++ có chung giới hạn hardcode và đều dừng ở 61.000). Khi vượt ngưỡng này, server ép client ngắt kết nối và kết nối lại, làm phát sinh lỗi vượt mức 1%. Nếu bỏ dòng giới hạn đó đi, Zig hoàn toàn có thể chạm trần. - Đồng quán quân: C++ và Rust (64.000 concurrent users - Chạm trần giới hạn hệ điều hành):
Tại mốc 64.000 users, cả hai ngôn ngữ này đều đụng phải một bức tường vô hình: bảng Connection Tracking Table (conntrack) của tường lửa Linux. Trên máy ảo này, bảng conntrack được cấu hình mặc định ở mức khoảng 65.000 kết nối. Khi vượt quá 64.000 virtual users giữ kết nối mở, bảng này bị đầy và Linux kernel bắt đầu tự động drop các gói tin (packets). Vì luật chơi không cho phép can thiệp tinh chỉnh tham số của Kernel (sysctl), 64.000 trở thành điểm số tối đa trên thực tế. - Cách tiếp cận của C++ (64.000 users): Chạy 1 thread, 1 event loop, không worker pool, không lock. Khi profile và thấy SQLite tốn 13% CPU chỉ để quét bộ nhớ đệm trang (page cache) nội bộ khi commit, tác giả đã thu nhỏ cache nội bộ của SQLite về gần như bằng không, và dùng kỹ thuật Memory-mapped I/O (mmap) để việc đọc dữ liệu tận dụng trực tiếp Page Cache của hệ điều hành Linux. Bên cạnh đó, thao tác WAL checkpoint được dời ra khỏi luồng trả kết quả request, và cấu hình WAL được đặt ở chế độ synchronous = NORMAL (chấp nhận đánh đổi một chút rủi ro mất dữ liệu nếu sập nguồn đột ngột để đổi lấy tốc độ). Ở 64.000 users, C++ có p99 latency là 910ms và dùng 68% CPU.
- Cách tiếp cận của Rust (64.000 users - Chiến thắng chung cuộc): Bản Rust thứ nhất dùng framework Axum đạt 43.000 users (tốn tới 1.1GB RAM). Nhưng bản Rust thứ hai đã tự viết hệ thống dựa trên io_uring ở chế độ cho phép nhân Linux quản lý trực tiếp các tham chiếu socket (tránh cả chi phí tra cứu bảng file descriptor thông thường), kết hợp gom batch events trong event loop và Group Commit. Kết quả ở mức 64.000 users thật sự đáng kinh ngạc: p99 latency chỉ có 224ms (thấp hơn rất nhiều so với 910ms của C++), RAM tốn dưới 100MB, và CPU mới chỉ chạy hết 52%. Nhờ dư địa phần cứng còn trống quá lớn, Rust được trao ngôi vị quán quân tuyệt đối.
- Ruby (14.000 concurrent users):
5 bài học kỹ thuật đắt giá và góc nhìn thực tế (Trade-offs)
Nhìn lại toàn bộ các bản cài đặt đạt điểm cao nhất, bạn sẽ thấy có 5 mẫu số chung luôn lặp đi lặp lại:
- Bỏ qua Reverse Proxy (Nginx): Nginx cực kỳ hữu ích trên thực tế (quản lý TLS, Load Balancing, Routing), nhưng trên một máy chủ chỉ có đúng 1 nhân CPU, việc chạy thêm một tiến trình trung gian sẽ gây tốn kém tài nguyên di chuyển dữ liệu và context switch. Các bản nhanh nhất đều mở port đón HTTP traffic trực tiếp.
- Prepared Statements và Single Connection: Khởi tạo các câu lệnh SQL đúng một lần khi bật app và tái sử dụng thông qua một kết nối duy nhất để loại bỏ hoàn toàn chi phí parse lại câu lệnh và tránh tranh chấp khóa (lock contention).
- Đẩy việc tạo JSON xuống Database: Bỏ qua bước ánh xạ từ dòng dữ liệu sang Object trên tầng ứng dụng rồi mới Serialize sang JSON. Để SQLite làm việc đó và chỉ truyền mảng byte thô về cho người dùng.
- Group Commit: Gom hàng trăm thao tác ghi từ các request khác nhau vào chung một transaction để chia sẻ chi phí I/O khi ghi xuống đĩa.
- Loại bỏ Framework (Framework-less): Các bản dẫn đầu đều vứt bỏ các Web Framework thông thường để tự viết lấy bộ xử lý socket, tự quản lý vùng nhớ và event loop riêng.
- Bản C++ chiến thắng nhồi nhét hơn 1.300 dòng code vào một file duy nhất, tự viết lại cả bộ parser cho HTTP và JSON.
- Bản Rust nhanh nhất dài 2.400 dòng code với hàng loạt khối lệnh unsafe và phụ thuộc chặt vào các tính năng mới của nhân Linux.
- Bản TypeScript trên Bun chỉ cắt ghép chuỗi byte vừa đủ cho 5 endpoints này, hoàn toàn mất đi khả năng mở rộng và bảo mật tiêu chuẩn của giao thức HTTP.
- Ngay cả kỹ thuật rất thực tế như Group Commit trên một kết nối duy nhất cũng mang lại rủi ro: nếu một transaction bị lỗi, hàng trăm request đang đợi chung trong batch đó sẽ bị đánh rớt cùng lúc, hoặc chỉ cần một câu query chạy chậm sẽ làm nghẽn toàn bộ hệ thống phía sau.
-------------
Anh chị em thấy ấn tượng với màn trình diễn của ngôn ngữ nào nhất trong đợt benchmark này? Và nếu viết bằng Java hay Spring Boot với Virtual Threads hoặc Netty thuần thì liệu sẽ trụ được ở mốc bao nhiêu trên con server 1 core 2GB RAM này? Cùng để lại bình luận trao đổi nhé!






