A Saga de Como Turbinamos Nosso Sistema Ruby on Rails de 20 Mil para 500 Mil Requisições Simultanêas Sem Gastar Um Tostão a Mais na Infra!
Lê Lân
0
Tối Ưu Ứng Dụng Ruby on Rails Để Xử Lý 500k Yêu Cầu Đồng Thời: Hành Trình Từ 20k Đến 500k
Mở Đầu
Đạt được khả năng mở rộng quy mô xử lý đồng thời hàng trăm ngàn yêu cầu luôn là một thử thách lớn đối với nhiều ứng dụng web, đặc biệt khi tài nguyên hạ tầng có giới hạn.
Khi bắt đầu dự án này, hệ thống Ruby on Rails của chúng tôi chỉ có thể chịu được khoảng 20.000 yêu cầu đồng thời, điều này không đáp ứng được tiêu chí chuẩn bị cho đợt thử nghiệm Proof of Concept (POC). Ứng dụng liên quan đến việc xử lý các bản ghi y tế, sử dụng OpenAI để chuyển đổi các yêu cầu khám thành các biểu mẫu phát hiện cấu trúc. Qua quá trình kiểm tra tải, chúng tôi nhận thấy hệ thống cũ không thể chịu tải cao hơn.
Bài viết này sẽ trình bày chi tiết hành trình phân tích, tối ưu và áp dụng các kiến thức về hệ điều hành như đồng thời (concurrency), song song (parallelism), quản lý tiến trình và luồng (processes & threads) để từ đó nâng cao hiệu suất lên mức xử lý 500.000 yêu cầu đồng thời trên cùng một hạ tầng hiện có, tiết kiệm chi phí nhưng vẫn hiệu quả vượt trội.
1. O Diagnóstico: Xác Định Các Nút Thắt Hiệu Suất
1.1 Thử Nghiệm Tải Ban Đầu Với Locust
Để hiểu rõ các điểm yếu của ứng dụng, chúng tôi sử dụng Locust - một công cụ test tải thân thiện và dễ sử dụng - tạo các kịch bản mô phỏng hành vi người dùng thực tế.
Khi yêu cầu đồng thời ở mức trung bình, hệ thống phản hồi tốt
Nhưng khi tăng lên, thời gian phản hồi bắt đầu tăng dần theo cấp số nhân
Ở mức 20.000 yêu cầu đồng thời, ứng dụng bắt đầu xuất hiện lỗi timeout, lỗi kết nối reset, và cuối cùng không thể phục vụ bất kỳ yêu cầu nào
1.2 Kết Luận Về Kiến Trúc Hệ Thống
Ứng dụng hiện tại chưa tối ưu cho việc quản lý kết nối và xử lý song song, dẫn đến tình trạng quá tải gây hiệu ứng domino trên toàn hệ thống.
Những vấn đề chính:
Xử lý các yêu cầu dạng khối (blocking) một cách tuần tự
Hệ thống không tận dụng tốt đa nhân CPU
Bộ đệm và cấu hình proxy không phù hợp với khối lượng lớn kết nối đồng thời
2. Từ Khái Niệm Đến Thực Tiễn: Concurrency, Parallelism, Processes và Threads
2.1 Hiểu Về Concurrency (Đồng Thời)
Concurrency cho phép nhiều tác vụ “chia sẻ” thời gian xử lý nhưng không thực sự chạy đồng thời cùng lúc.
Ví dụ: Người phục vụ trong nhà hàng phải làm tuần tự các việc như nhận order, đem món, thu tiền, nhưng có thể chuyển đổi nhanh giữa các công việc.
2.2 Parallelism (Song Song) - Bước Chuyển Mình
Parallelism thực sự chạy nhiều tác vụ cùng lúc nhờ vào nhiều nhân CPU hoặc nhiều tiến trình.
Ví dụ: Nhà hàng thuê thêm nhiều người phục vụ mỗi người có thể làm độc lập đồng thời, tăng năng suất phục vụ.
2.3 Processes và Threads
Thuật ngữ
Ý nghĩa
Đặc điểm chính
Process
Tiến trình độc lập với bộ nhớ riêng
Tốn tài nguyên, bảo mật tốt
Thread
Luồng là thành phần nhỏ hơn trong process
Chia sẻ bộ nhớ, nhẹ hơn, dễ phối hợp
Mỗi process như một “nhà bếp” riêng, threads là “đầu bếp” hoạt động bên trong nhà bếp đó.
2.4 Áp Dụng Vào Ứng Dụng Ruby on Rails
Ruby on Rails ban đầu không tận dụng hết đồng thời và song song do chạy trên môi trường đa tiến trình / luồng chưa được tối ưu, gây ra hiện tượng “tắc nghẽn”.
3. Chiến Lược Tối Ưu: Tối Đa Hóa Tài Nguyên Hạ Tầng
3.1 Tính Toán Số Lượng Process và Thread Tối Ưu
Căn cứ vào số lõi CPU vật lý, áp dụng công thức:
Số process = số lõi CPU vật lý
Số thread/process = 2 - 4
Ví dụ: Server 8 lõi → cấu hình 8 workers Puma & 16-32 threads total
workers ENV.fetch("WEB_CONCURRENCY") { 8 } # Worker = Process
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 16 } # Threads per process
threads threads_count, threads_count
preload_app!
Sử dụng công cụ htop hoặc top trên Linux để theo dõi hiệu suất CPU và điều chỉnh cho phù hợp với profile tải thực tế.
3.2 Tối Ưu Server Puma Và Sidekiq
Puma hỗ trợ multi-threading giúp xử lý nhiều kết nối cùng lúc hiệu quả mà không tạo tốn tải quá lớn
Sidekiq và Redis được dùng cho xử lý các tác vụ background (ví dụ: transcribing y tế qua OpenAI), tránh làm nghẽn luồng chính của ứng dụng
3.3 Điều Chỉnh Cấu Hình NGINX
Tham số
Giá trị
Ý nghĩa
worker_processes
auto
Theo số lõi CPU
worker_connections
8192
Kết nối tối đa mỗi worker
multi_accept
on
Nhận nhiều kết nối cùng lúc
proxy_connect_timeout
60s
Timeout kết nối proxy
proxy_send_timeout
60s
Timeout gửi dữ liệu proxy
proxy_read_timeout
60s
Timeout đọc dữ liệu proxy
proxy_buffer_size
128k
Buffer nhận phản hồi
proxy_buffers
4 256k
Bộ đệm chiều rộng
proxy_busy_buffers_size
256k
Bộ đệm bận
Cấu hình này giúp NGINX xử lý nhiều kết nối đồng thời và ổn định hơn, giảm lỗi 502 Bad Gateway hay timeout.
4. Kết Quả Và Bài Học
4.1 Hiệu Quả Sau Khi Tối Ưu
Khi tái thực hiện kiểm thử tải với Locust sau điều chỉnh:
Khả năng chịu tải đồng thời tăng từ 20k lên 500k + yêu cầu
Độ trễ trung bình giảm đáng kể
Ứng dụng hoạt động ổn định, không gặp lỗi time-out hay lỗi mạng phổ biến
4.2 Bài Học Rút Ra
Tối ưu đồng thời và song song là chìa khóa cho mở rộng ứng dụng quy mô lớn trên cùng hạ tầng
Hiểu rõ quá trình xử lý ở cấp S.O (processes, threads) giúp thiết kế hệ thống hiệu quả hơn
Sử dụng các công cụ kiểm thử và giám sát giúp phát hiện sớm điểm nghẽn và điều chỉnh kịp thời
Kết Luận
Nhờ áp dụng kiến thức cơ bản nhưng quan trọng về đồng thời, song song, và tối ưu cấu hình của Puma, Sidekiq, Redis, cùng với điều chỉnh NGINX hợp lý, chúng tôi đã nâng khả năng xử lý của ứng dụng Ruby on Rails từ 20.000 lên hơn 500.000 yêu cầu đồng thời, tận dụng tối đa tài nguyên sẵn có mà không cần tăng thêm hạ tầng.
Nếu bạn đang gặp vấn đề tương tự về mở rộng quy mô, hãy bắt đầu từ việc đánh giá kiến trúc xử lý song song trong ứng dụng của mình và thử áp dụng các giải pháp đơn giản như trên để tăng hiệu suất một cách bền vững!