Chương 3. Ba Con Số Phải Luôn Nghĩ Đến
Khi một hệ thống chậm, nhiều người sẽ nói ngay:
- Phải đổi framework.
- Phải thêm server.
- Phải dùng microservices.
- Phải dùng cache.
- Phải dùng async.
- Phải dùng queue.
Nhưng trước khi chọn giải pháp, ta cần hiểu hệ thống đang chậm theo kiểu nào.
Có ba con số nền tảng gần như luôn phải nghĩ đến:
- Latency: một việc mất bao lâu.
- Throughput: trong một khoảng thời gian xử lý được bao nhiêu việc.
- Concurrency: cùng lúc có bao nhiêu việc đang diễn ra.
Ba con số này đơn giản, nhưng nếu hiểu sâu, ta sẽ tránh được rất nhiều quyết định kiến trúc sai.
---
3.1. Ví dụ quán bánh
Tiếp tục ví dụ website bán bánh ở Chương 2, hãy tạm rời khỏi máy chủ và nhìn vào một quán bánh thật.
Quán có một nhân viên nhận đơn.
Mỗi khách vào mua bánh cần:
- 1 phút để chọn bánh.
- 1 phút để nhân viên đóng gói.
- 1 phút để thanh toán.
Tổng cộng một khách mất khoảng 3 phút.
Ở đây:
- Latency là 3 phút/khách.
- Throughput là số khách phục vụ được mỗi giờ.
- Concurrency là số khách đang được phục vụ hoặc đang chờ trong cùng một thời điểm.
Nếu chỉ có một nhân viên, mỗi khách mất 3 phút, thì trong một giờ phục vụ được khoảng:
60 / 3 = 20 khách/giờ
Nếu có 3 nhân viên, mỗi khách vẫn mất 3 phút, nhưng quán có thể phục vụ song song 3 khách:
3 * 60 / 3 = 60 khách/giờ
Điểm quan trọng:
> Tăng số người phục vụ không làm một khách nhanh hơn, nhưng làm cả quán phục vụ được nhiều khách hơn.
Đây là khác biệt giữa latency và throughput.
---
3.2. Latency: một việc mất bao lâu
Latency là thời gian để hoàn thành một việc.
Trong web app:
- Mở trang mất 300ms.
- Query database mất 50ms.
- Gọi API thanh toán mất 2 giây.
- Upload file mất 20 giây.
- Chấm AI mất 90 giây.
Latency trả lời câu hỏi:
Một request/job cụ thể mất bao lâu?
Latency ảnh hưởng trực tiếp đến trải nghiệm người dùng. Nếu người dùng bấm nút và phải chờ 10 giây, họ thấy chậm. Nếu phải chờ 90 giây, họ có thể nghĩ hệ thống treo.
Nhưng latency không chỉ đến từ code.
Nó có thể đến từ:
- Network.
- Database.
- Cache miss.
- File lớn.
- External API.
- Lock trong database.
- Queue chờ trước khi được xử lý.
- CPU đang quá tải.
- Proxy timeout.
Vì vậy khi thấy "request mất 5 giây", đừng vội kết luận backend chậm. Phải hỏi 5 giây đó nằm ở đâu.
---
3.3. Latency trung bình có thể lừa ta
Giả sử hệ thống có latency trung bình:
200ms
Nghe rất tốt. Nhưng thực tế có thể là:
- 90% request mất 100ms.
- 9% request mất 500ms.
- 1% request mất 10 giây.
Trung bình vẫn có thể trông ổn, nhưng 1% người dùng đang có trải nghiệm rất tệ.
Vì vậy trong production, người ta hay nhìn:
- p50: 50% request nhanh hơn mức này.
- p95: 95% request nhanh hơn mức này.
- p99: 99% request nhanh hơn mức này.
Ví dụ:
p50 = 120ms
p95 = 800ms
p99 = 5s
Điều này nói rằng đa số request nhanh, nhưng nhóm chậm nhất rất đáng chú ý.
Tư duy thực tế:
> Người dùng không sống trong latency trung bình. Người dùng sống trong request cụ thể của họ.
Nếu request của họ rơi vào p99, họ sẽ thấy hệ thống rất tệ dù dashboard trung bình vẫn đẹp.
---
3.4. Throughput: mỗi giây/phút xử lý được bao nhiêu việc
Throughput là số lượng việc hệ thống xử lý được trong một khoảng thời gian.
Ví dụ:
- 100 request/giây.
- 1.000 email/phút.
- 30 file/phút.
- 60 đơn hàng/phút.
- 100 job AI/phút.
Throughput trả lời câu hỏi:
Hệ thống xử lý được bao nhiêu việc trong một đơn vị thời gian?
Quay lại quán bánh:
- Một nhân viên xử lý 1 khách trong 3 phút.
- Throughput của một nhân viên là 20 khách/giờ.
- Ba nhân viên là 60 khách/giờ.
Trong server cũng vậy.
Nếu một worker xử lý một job mất 10 giây, một worker xử lý được:
60 / 10 = 6 job/phút
Nếu có 5 worker:
5 * 6 = 30 job/phút
Throughput không chỉ phụ thuộc vào tốc độ một job, mà còn phụ thuộc vào số việc có thể làm song song.
---
3.5. Concurrency: cùng lúc có bao nhiêu việc đang diễn ra
Concurrency là số việc đang được xử lý hoặc đang chờ xử lý cùng lúc.
Ví dụ:
- 100 người đang mở website cùng lúc.
- 50 request đang được backend xử lý.
- 200 job đang nằm trong queue.
- 30 request đang chờ API bên ngoài trả lời.
- 1.000 WebSocket connection đang mở.
Concurrency trả lời câu hỏi:
Cùng một thời điểm có bao nhiêu việc đang diễn ra?
Điểm dễ nhầm:
> Concurrency không có nghĩa là mọi việc đều đang dùng CPU.
Nhiều việc có thể chỉ đang chờ:
- Chờ database.
- Chờ API ngoài.
- Chờ file upload.
- Chờ network.
- Chờ Gemini/OpenAI/Stripe/Email provider.
Với I/O-bound workload, rất nhiều thời gian là chờ. Vì vậy nếu hệ thống chỉ có vài slot chờ, throughput sẽ rất thấp dù CPU còn rảnh.
---
3.6. Ví dụ chấm AI 90 giây
Giả sử một job chấm bài bằng AI mất:
90 giây
Nếu hệ thống chỉ xử lý được 4 job cùng lúc:
concurrency = 4
latency = 90 giây
Throughput tối đa là:
4 job / 90 giây = 0.044 job/giây
0.044 * 60 = 2.67 job/phút
Tức là hệ thống chỉ xử lý khoảng 2-3 bài mỗi phút.
Nếu tăng concurrency lên 50:
50 job / 90 giây * 60 = 33.3 job/phút
Nếu concurrency là 150:
150 job / 90 giây * 60 = 100 job/phút
Điểm quan trọng:
> Một job vẫn mất 90 giây. Nhưng toàn hệ thống xử lý được nhiều job hơn trong cùng một khoảng thời gian.
Tăng concurrency không làm một bài chấm nhanh hơn. Nó làm nhiều bài được chấm song song hơn.
---
3.7. Công thức dễ nhớ
Công thức cơ bản:
throughput = concurrency / latency
Nếu tính theo phút:
throughput_per_minute = concurrency * 60 / latency_seconds
Suy ngược lại:
concurrency_needed = target_throughput_per_minute * latency_seconds / 60
Ví dụ muốn xử lý 100 job/phút, mỗi job mất 90 giây:
concurrency_needed = 100 * 90 / 60 = 150
Đây là cách tính rất thực dụng. Nó giúp ta không nói mơ hồ kiểu:
- "Cần scale lên."
- "Cần async."
- "Cần microservice."
- "Cần thêm server."
Mà nói cụ thể:
Muốn đạt 100 job/phút với latency 90 giây, cần khoảng 150 job chạy song song.
Sau đó mới hỏi: đạt concurrency 150 bằng cách nào?
- Nhiều process?
- Nhiều thread?
- Async/event loop?
- Nhiều worker?
- Nhiều machine?
- Hay giảm latency của mỗi job?
---
3.8. Concurrency không phải thread, process hay async
Concurrency là mục tiêu: cùng lúc có bao nhiêu việc.
Thread, process, async là cách để đạt concurrency.
Ví dụ cần 100 request đang chờ API ngoài cùng lúc.
Có thể đạt bằng:
Nhiều process
100 process, mỗi process chờ 1 request
Dễ hiểu, cách ly tốt, nhưng tốn RAM.
Nhiều thread
1 hoặc vài process, mỗi process có nhiều thread
Phù hợp với việc chờ I/O, nhẹ hơn process, nhưng vẫn cần quản lý thread pool.
Async/event loop
1 event loop quản lý nhiều request đang chờ
Rất hợp I/O-bound nếu toàn bộ code và thư viện hỗ trợ non-blocking đúng cách.
Greenlet/coroutine nhẹ
Một số runtime dùng greenlet, coroutine hoặc virtual thread để có nhiều "luồng nhẹ" hơn thread hệ điều hành.
Điểm chính:
> Đừng hỏi "concurrency có phải thread không?". Hãy hỏi "mình cần bao nhiêu việc chạy/chờ song song, và runtime nào đạt được mức đó với chi phí hợp lý?"
---
3.9. Latency cao không luôn là vấn đề, thiếu concurrency mới là vấn đề
Một job mất 90 giây nghe rất lâu. Nhưng nếu đó là việc nền, người dùng không cần chờ trực tiếp, thì 90 giây có thể chấp nhận được.
Vấn đề là nếu hệ thống chỉ xử lý 4 job cùng lúc, queue sẽ dài rất nhanh.
Ví dụ:
- 100 học viên cùng nộp bài.
- Mỗi bài mất 90 giây.
- Hệ thống xử lý 4 bài cùng lúc.
Số lượt xử lý cần:
100 / 4 = 25 lượt
Mỗi lượt 90 giây:
25 * 90 = 2250 giây = 37.5 phút
Người cuối có thể phải chờ gần 40 phút.
Nếu concurrency là 50:
100 / 50 = 2 lượt
2 * 90 = 180 giây = 3 phút
Một job vẫn mất 90 giây, nhưng trải nghiệm toàn hệ thống khác hoàn toàn.
Tư duy:
> Với việc nền dài, hãy nhìn queue wait time, không chỉ job duration.
Người dùng cảm nhận:
tổng thời gian = thời gian chờ trong queue + thời gian xử lý job
Nếu job mất 90 giây nhưng nằm chờ queue 30 phút, vấn đề lớn nhất không phải AI chấm chậm, mà là hệ thống thiếu capacity/concurrency.
---
3.10. Throughput có thể tăng bằng những cách nào?
Muốn tăng throughput, có vài hướng.
Giảm latency mỗi việc
Ví dụ:
- Tối ưu query database.
- Giảm số API call.
- Tối ưu prompt.
- Batch request.
- Dùng model nhanh hơn.
- Cache kết quả.
Nếu mỗi job từ 90 giây giảm còn 45 giây, throughput tăng gấp đôi với cùng concurrency.
Tăng concurrency
Ví dụ:
- Tăng số worker.
- Tăng thread pool.
- Dùng async runtime.
- Chạy nhiều instance.
- Tách queue riêng cho workload nặng.
Nếu latency không giảm được, tăng concurrency là cách tăng throughput.
Loại bỏ việc không cần làm
Ví dụ:
- Không gửi email trùng.
- Không xử lý lại file đã xử lý.
- Không gọi AI nếu nội dung không đổi.
- Không query database nếu cache hợp lệ.
Việc nhanh nhất là việc không phải làm.
Chuyển việc sang thời điểm khác
Ví dụ:
- Generate report ban đêm.
- Xử lý batch khi hệ thống rảnh.
- Precompute kết quả.
- Warm cache trước giờ cao điểm.
Không phải mọi việc phải xử lý ngay lúc người dùng bấm.
---
3.11. Concurrency quá cao cũng có thể làm chết hệ thống
Tăng concurrency không phải lúc nào cũng tốt.
Nếu tăng quá cao, hệ thống có thể nghẽn ở nơi khác:
- Database connection pool cạn.
- API ngoài bị rate limit.
- RAM tăng mạnh.
- CPU tăng do context switching.
- Queue consumer ghi quá nhiều vào database.
- Provider tính phí tăng vọt.
Ví dụ:
Bạn tăng worker concurrency từ 4 lên 200 để gọi API ngoài. Nếu mỗi job sau khi gọi API xong đều ghi database, có thể 200 job cùng ghi làm database nghẽn.
Hoặc provider cho 100 request/phút, nhưng bạn bắn 300 request/phút, hệ thống nhận lỗi 429 hàng loạt. Sau đó retry không kiểm soát, tạo retry storm.
Tăng concurrency phải đi cùng:
- Rate limit.
- Connection pool phù hợp.
- Timeout.
- Backpressure.
- Monitoring.
- Cost tracking.
Nguyên tắc:
> Concurrency là sức mạnh, nhưng cũng là áp lực. Tăng nó mà không kiểm soát sẽ chuyển bottleneck sang chỗ khác.
---
3.12. Queue wait time: con số dễ bị quên
Với job nền, có ba thời gian:
Tổng thời gian = thời gian chờ trong queue + thời gian xử lý + thời gian báo kết quả
Nhiều người chỉ đo "job xử lý mất bao lâu" mà quên đo "job nằm chờ bao lâu".
Ví dụ:
- Job xử lý: 90 giây.
- Job chờ trong queue: 20 phút.
Nếu chỉ nhìn job duration, ta nghĩ hệ thống ổn. Nhưng người dùng chờ 21 phút 30 giây.
Các chỉ số nên theo dõi:
- Queue length: có bao nhiêu job đang chờ.
- Queue age: job cũ nhất đã chờ bao lâu.
- Queue wait time p95/p99.
- Worker busy/idle.
- Job duration p95/p99.
- Retry count.
- Dead letter count.
Queue là một cái hồ chứa. Nếu nước vào nhanh hơn nước ra, hồ sẽ đầy. Queue length tăng là dấu hiệu throughput xử lý thấp hơn throughput đầu vào.
---
3.13. Backpressure: biết từ chối hoặc làm chậm lại
Nếu hệ thống nhận việc nhanh hơn khả năng xử lý, queue sẽ phình mãi.
Backpressure là cơ chế nói:
Tôi đang quá tải, đừng gửi thêm nhanh như vậy.
Backpressure có thể là:
- Trả lỗi 429 Too Many Requests.
- Giới hạn số job mỗi user.
- Tạm ngưng nhận upload.
- Đưa user vào waiting room.
- Giảm chất lượng xử lý.
- Chuyển job sang batch sau.
- Ưu tiên job trả phí/cấp cao.
Không có backpressure, hệ thống có thể giả vờ vẫn nhận việc, nhưng thực tế chỉ đang tích nợ trong queue.
Tư duy thực tế:
> Một hệ thống tốt không phải lúc nào cũng nhận mọi thứ. Một hệ thống tốt biết khi nào phải làm chậm, từ chối hoặc hạ cấp để không sập toàn bộ.
---
3.14. Ví dụ: mở rộng quán bánh
Quay lại quán bánh.
Nếu khách đông và mỗi khách mất 3 phút, có nhiều cách cải thiện.
Giảm latency
- Menu rõ hơn để khách chọn nhanh.
- Bánh được chuẩn bị sẵn.
- Thanh toán nhanh hơn.
Mỗi khách từ 3 phút còn 1 phút.
Tăng concurrency
- Mở thêm quầy.
- Thêm nhân viên.
- Cho khách xếp nhiều hàng.
Một khách vẫn mất 3 phút, nhưng nhiều khách được phục vụ song song.
Tách việc lâu ra sau
- Khách đặt bánh sinh nhật, không chờ làm tại chỗ.
- Nhân viên nhận đơn, hẹn giờ lấy.
- Bếp xử lý đơn ở nền.
Đây chính là queue/worker.
Backpressure
- Khi quá đông, quán ngừng nhận đơn mới.
- Hoặc báo thời gian chờ 2 giờ.
- Hoặc chỉ nhận đơn đã đặt trước.
Đây chính là rate limit/waiting room.
Kiến trúc hệ thống cũng vậy. Các khái niệm nghe kỹ thuật nhưng bản chất rất đời thường.
---
3.15. Cách dùng ba con số khi thiết kế hệ thống
Khi thiết kế một tính năng, hãy hỏi:
Một việc mất bao lâu?
Đây là latency.
- Request đọc dữ liệu: 100ms hay 2s?
- Job xử lý file: 10s hay 10 phút?
- API ngoài: 500ms hay 90s?
Mỗi phút có bao nhiêu việc đến?
Đây là throughput đầu vào.
- 10 request/giây?
- 1.000 email/phút?
- 100 bài nộp/phút?
Cùng lúc cần bao nhiêu việc đang xử lý?
Đây là concurrency cần thiết.
Dùng công thức:
concurrency = throughput * latency
Nhớ cùng đơn vị thời gian.
Ví dụ:
throughput = 100 job/phút
latency = 90 giây = 1.5 phút
concurrency = 100 * 1.5 = 150
Bottleneck sẽ nằm ở đâu?
- CPU?
- RAM?
- Database?
- API ngoài?
- Queue?
- Network?
- File storage?
Có cần trả kết quả ngay không?
- Nếu có: tối ưu request path.
- Nếu không: đưa ra queue/job nền.
Chỉ cần những câu hỏi này, quyết định kiến trúc đã sáng hơn rất nhiều.
---
3.16. Những lỗi tư duy phổ biến
Lỗi 1: Thấy latency cao là đổi framework
Nếu latency cao vì API ngoài chậm, đổi framework chưa chắc giúp. Cần xem có thể xử lý nền, tăng concurrency, hoặc giảm số lần gọi API không.
Lỗi 2: Tăng server nhưng quên database
Thêm web server có thể tăng request đổ vào database. Nếu database đã nghẽn, hệ thống có thể tệ hơn.
Lỗi 3: Tăng concurrency nhưng không giới hạn rate
Concurrency cao có thể bắn quá nhiều request vào provider, database hoặc queue khác.
Lỗi 4: Chỉ đo trung bình
Average latency đẹp không có nghĩa p95/p99 đẹp.
Lỗi 5: Không đo queue wait time
Job duration nhanh nhưng queue wait time dài thì người dùng vẫn chờ lâu.
Lỗi 6: Nhầm concurrency với speed
Concurrency cao không làm một job nhanh hơn. Nó làm nhiều job được xử lý song song hơn.
---
3.17. Kết luận của chương
Ba con số này là nền tảng của rất nhiều quyết định kiến trúc:
- Latency: một việc mất bao lâu.
- Throughput: mỗi đơn vị thời gian xử lý được bao nhiêu việc.
- Concurrency: cùng lúc có bao nhiêu việc đang diễn ra.
Nếu hiểu ba con số này, ta sẽ bớt bị cuốn vào các câu trả lời mơ hồ như "dùng async", "dùng microservices", "thêm server", "đổi framework".
Ta sẽ hỏi cụ thể hơn:
- Việc này mất bao lâu?
- Mỗi phút có bao nhiêu việc đến?
- Cần bao nhiêu việc chạy song song?
- Queue đang chờ bao lâu?
- Bottleneck nằm ở đâu?
- Tăng concurrency có làm nghẽn database/API ngoài không?
Kiến trúc tốt bắt đầu từ khả năng nhìn hệ thống bằng số liệu đơn giản. Không cần công thức phức tạp. Chỉ cần biết latency, throughput và concurrency, ta đã có một chiếc la bàn rất mạnh để không đi lạc.