Họ đang đo gì
Bạn có phân biệt tác vụ nặng CPU với tác vụ chờ I/O không — nền tảng của mọi quyết định về đồng thời.
Trả lời ngắn~30 giây
Với tác vụ nặng CPU: bằng số lõi, cộng thêm không giúp gì vì không có lõi nào rảnh. Với tác vụ chờ I/O: số lõi × (1 + thời gian chờ / thời gian tính) — nếu 90% thời gian là chờ mạng thì con số này lớn hơn số lõi cả chục lần. Nhưng đừng dừng ở công thức: giới hạn thật thường nằm ở phía DƯỚI (pool database, hạn mức của API bên ngoài), nên tính ra 200 luồng mà database chỉ nhận 20 kết nối thì bạn chỉ đang xếp hàng dài hơn.
Giải thích sâu
Điều đáng nói là hàng đợi cũng là một tham số, và thường quan trọng hơn số luồng. Một pool 10 luồng với hàng đợi vô hạn sẽ không bao giờ từ chối việc — nó chỉ tích tụ cho tới khi hết bộ nhớ, và độ trễ tăng không giới hạn trong lúc mọi thứ trông vẫn “bình thường”. Hàng đợi CÓ GIỚI HẠN kèm chính sách từ chối rõ ràng (trả 503, hoặc đẩy ngược lên caller) làm hệ thống thất bại theo cách nhìn thấy được, và đó là điều bạn muốn.
Cái bẫy kinh điển là dùng CHUNG một pool cho tác vụ chặn và tác vụ không chặn. Một tác vụ gọi API bên ngoài chiếm luồng suốt thời gian chờ mạng; đủ nhiều tác vụ như vậy là pool cạn và những tác vụ nhanh cũng không chạy được. Cách chữa là bulkhead: pool riêng cho từng loại phụ thuộc, để một dịch vụ chậm không kéo theo cả hệ thống.
Với JVM hiện đại thì câu hỏi này đang đổi hình dạng: virtual thread làm việc chặn rẻ tới mức pool gần như không còn cần cho tác vụ I/O. Nhưng nguyên lý vẫn giữ nguyên — bạn vẫn phải giới hạn đồng thời ở nơi có giới hạn thật, chỉ là bằng semaphore thay vì bằng kích thước pool. Nói được điều đó cho thấy bạn hiểu vấn đề chứ không thuộc công thức.
Câu hỏi tiếp theo họ sẽ hỏi
?Đo bằng chỉ số nào để biết pool sai kích thước?
Độ dài hàng đợi và thời gian chờ trong hàng đợi. Hàng đợi luôn rỗng mà CPU chưa bão hoà thì có thể tăng; hàng đợi dài mà CPU đã đầy thì tăng luồng chỉ làm mọi thứ chậm đều. Số luồng đang bận một mình không nói lên gì.
Trả lời thế này là mất điểm
- Dùng
Executors.newCachedThreadPool()cho tải không kiểm soát. Nó tạo luồng không giới hạn và sẽ giết JVM dưới đợt tải đột biến.
Nguồn
- Brian Goetz, Java Concurrency in Practice, ch. 8 — kích thước pool và hàng đợi
- Java SE API — `ThreadPoolExecutor` (chính sách từ chối)