Một hệ thống không hỏng chỉ vì lưu lượng lớn; nó thường hỏng vì tiếp nhận công việc nhanh hơn tốc độ xử lý trong thời gian đủ lâu. Request, message hoặc chunk dữ liệu chưa được xử lý phải nằm ở đâu đó: bộ nhớ ứng dụng, connection pool, hàng đợi broker, socket buffer hoặc hàng chờ của dependency. Nếu các vùng đệm này không có giới hạn và không tạo tín hiệu ngược, độ trễ tăng dần, bộ nhớ phình to, timeout kéo theo retry, rồi toàn bộ hệ thống rơi vào vòng khuếch đại tải.
Backpressure là cơ chế để phía xử lý chậm truyền tín hiệu về phía tạo tải: hãy chậm lại, chờ, giảm số lượng đang bay hoặc từ chối có kiểm soát. Đây không phải một thư viện đơn lẻ mà là nguyên tắc thiết kế xuyên suốt từ HTTP ingress, ứng dụng, database đến message queue và worker. Bài viết trình bày cách xác định điểm nghẽn, đặt giới hạn hữu hạn, chọn chính sách phản hồi và kiểm thử hệ thống khi vượt công suất.
1. Backpressure giải quyết vấn đề nào?
Hãy xét một API nhận 500 request mỗi giây nhưng dependency phía sau chỉ hoàn thành ổn định 300 request mỗi giây. Chênh lệch 200 request mỗi giây không biến mất. Sau một phút đã có 12.000 công việc chờ, chưa tính retry. Nếu ứng dụng giữ mỗi request cùng context, payload và promise trong RAM, quá tải sẽ chuyển từ độ trễ thành garbage collection dày đặc, rồi OOM hoặc restart. Nếu đẩy tất cả sang broker, backlog vẫn tăng và thời gian hoàn tất vượt xa kỳ vọng người dùng.
Backpressure buộc hệ thống thừa nhận công suất hữu hạn. Khi đạt ngưỡng, nó có thể tạm dừng đọc, giảm concurrency, không lấy thêm message, trả lỗi có thể retry, hoặc chuyển sang một chế độ suy giảm chức năng. Mục tiêu không phải làm biến mất tải mà giữ hệ thống trong miền vận hành có thể dự đoán: bộ nhớ có trần, thời gian chờ có hạn, dependency không bị ép quá mức và người gọi nhận được kết quả rõ ràng.
| Cơ chế | Điều nó kiểm soát | Nếu thiếu |
|---|---|---|
| Bounded queue | Số công việc chờ trong tiến trình | Bộ nhớ tăng cho đến khi tiến trình mất ổn định |
| Concurrency limit | Số tác vụ dùng CPU hoặc dependency cùng lúc | Pool cạn, context switching và timeout tăng |
| Admission control | Công việc mới có được nhận hay không | Mọi request cùng chậm rồi cùng thất bại |
| Flow control | Tốc độ producer gửi dữ liệu cho consumer | Buffer tích tụ giữa các tầng |
| Deadline và cancellation | Công việc hết giá trị có tiếp tục chiếm tài nguyên không | Máy chủ xử lý kết quả không còn ai cần |
Cần phân biệt backpressure với autoscaling. Scale-out có thể nâng công suất nếu workload song song được và dependency còn dư địa, nhưng việc thêm instance cần thời gian và không giải quyết một database đã bão hòa. Backpressure là hàng rào an toàn tức thời; autoscaling là một phản ứng cung ứng tài nguyên chậm hơn. Hệ thống production thường cần cả hai.
2. Vẽ đường đi của tải và tìm hàng đợi ẩn
Trước khi chỉnh một tham số, hãy vẽ toàn bộ đường đi của một đơn vị công việc. Với HTTP, chuỗi có thể gồm load balancer, socket accept queue, middleware, executor, connection pool, database và dịch vụ downstream. Với worker, chuỗi có thể gồm broker partition, consumer prefetch, bộ đệm client, executor nội bộ và nơi lưu kết quả. Mỗi điểm có thể chứa hàng chờ dù tên cấu hình không có chữ “queue”.
Đối với từng điểm, ghi bốn thông tin: sức chứa tối đa, tốc độ vào, tốc độ ra và chính sách khi đầy. Một vùng đệm “tạm thời” nhưng không có giới hạn chính là nơi sự cố sẽ ẩn nấp. Tổng thời gian đáp ứng còn bao gồm thời gian chờ tại mọi tầng, vì vậy chỉ đo thời gian thực thi handler sẽ tạo cảm giác sai rằng ứng dụng vẫn nhanh.
- Socket và HTTP: bao nhiêu kết nối được chấp nhận, bao nhiêu request được xử lý đồng thời, keep-alive giữ tài nguyên bao lâu?
- Executor: hàng đợi task có hữu hạn không, task bị từ chối theo chính sách nào, công việc CPU và I/O có dùng chung pool không?
- Database: pool có bao nhiêu connection, thời gian chờ lấy connection là bao lâu, truy vấn có deadline không?
- Broker: consumer giữ bao nhiêu message chưa ack, kích thước message ra sao, backlog già đi với tốc độ nào?
- Downstream: client có giới hạn in-flight, timeout, retry budget và circuit breaker hay không?
Một nguyên tắc hữu ích là đặt hàng chờ gần nơi có đủ thông tin để ra quyết định. Broker bền vững phù hợp với công việc bất đồng bộ có thể xử lý muộn. Bộ nhớ ứng dụng chỉ phù hợp với lượng chờ nhỏ và ngắn. Không nên biến RAM của web server thành message queue ngoài ý muốn.
3. Dùng Little's Law để nhìn đúng quan hệ giữa tải và độ trễ
Trong một hệ thống ổn định, số công việc trung bình đang ở trong hệ thống xấp xỉ tốc độ hoàn tất nhân với thời gian lưu trú trung bình: L = λ × W. Công thức không thay thế benchmark nhưng giúp kiểm tra tính hợp lý. Nếu hoàn tất 200 request mỗi giây và mỗi request ở trong hệ thống 0,5 giây, sẽ có khoảng 100 request đang xử lý hoặc chờ tại một thời điểm.
Khi tốc độ đến vượt tốc độ hoàn tất kéo dài, hệ thống không còn ổn định và hàng chờ tăng liên tục. Tăng queue chỉ kéo dài thời gian trước khi thất bại; nó không tạo thêm throughput. Thậm chí hàng chờ lớn làm request cũ hết deadline trước khi được chạy, gây lãng phí CPU để xử lý một kết quả đã vô nghĩa.
Giới hạn tốt không được chọn theo lượng RAM còn trống. Nó phải xuất phát từ SLO độ trễ, công suất dependency, kích thước mỗi công việc và thời gian tối đa mà kết quả còn giá trị.
Ví dụ, nếu một endpoint có deadline đầu-cuối 800 ms và phần xử lý hữu ích thường cần 300 ms, hàng chờ không thể hợp lý ở mức vài giây. Admission control nên từ chối sớm khi thời gian chờ dự kiến đã ăn hết ngân sách. Từ chối nhanh thường tốt hơn để mọi request cùng timeout sau một quãng chờ dài.
4. Xây hàng đợi hữu hạn và giới hạn đồng thời
Hai núm điều khiển quan trọng nhất là độ dài hàng chờ và concurrency. Queue hấp thụ burst ngắn; concurrency quyết định bao nhiêu tác vụ cùng tranh CPU, connection và bandwidth. Tăng concurrency có ích đến điểm tài nguyên nghẽn. Sau điểm đó, throughput gần như không tăng trong khi latency, lỗi và chi phí chuyển ngữ cảnh tăng mạnh.
Không nên dùng cùng một semaphore cho mọi loại công việc. Request nhẹ đọc cache, request ghi database và tác vụ tạo báo cáo có hồ sơ tài nguyên khác nhau. Tách bulkhead giúp một nhóm chậm không chiếm toàn bộ slot. Đồng thời phải giữ một ngân sách tổng để nhiều pool riêng không cộng lại thành mức quá tải toàn hệ thống.
async function submit(task, deadline) {
if (deadline.expired()) return reject("deadline_exceeded");
if (!queue.tryPush({ task, deadline })) return reject("overloaded");
}
async function workerLoop() {
for await (const item of queue) {
if (item.deadline.expired()) continue;
await concurrencyPermit.run(() => item.task(item.deadline));
}
}
Đây là pseudocode để minh họa hợp đồng, không phải API của một runtime cụ thể. tryPush phải có giới hạn và trả kết quả ngay; permit phải được giải phóng trong nhánh finally; cancellation phải truyền xuống database hoặc HTTP client. Nếu timeout chỉ dừng chờ ở lớp ngoài nhưng truy vấn vẫn chạy, slot phía dưới vẫn bị chiếm và backpressure chỉ tồn tại trên biểu đồ.
Cũng cần xác định thứ tự ưu tiên. FIFO dễ hiểu nhưng có thể để một lô tác vụ nặng chặn request tương tác. Priority queue giúp phân lớp dịch vụ nhưng cần chống starvation và giới hạn riêng theo tenant. Fair queueing theo khách hàng hoặc workload tránh một producer ồn ào chiếm hết capacity.
5. Admission control cho HTTP API
Tại biên HTTP, hệ thống nên quyết định sớm liệu có đủ ngân sách để nhận request. Các tín hiệu có thể gồm số request in-flight, độ dài queue, thời gian chờ ước tính, mức sử dụng pool và trạng thái dependency. CPU đơn thuần thường là tín hiệu trễ; khi CPU tăng rõ rệt, hàng chờ có thể đã quá dài.
Khi từ chối do quá tải tạm thời, phản hồi phải nhất quán và không giả vờ thành công. Có thể dùng mã trạng thái phù hợp với hợp đồng API, kèm mã lỗi máy đọc được, correlation ID và hướng dẫn retry nếu thực sự an toàn. Nếu gửi Retry-After, giá trị phải dựa trên chính sách có cơ sở thay vì một con số trang trí. Request ghi cần idempotency key hoặc ràng buộc duy nhất trước khi khuyến khích client retry.
- Giới hạn kích thước body và tốc độ upload để client chậm không giữ connection vô hạn.
- Đặt deadline đầu-cuối và giảm ngân sách khi gọi qua từng downstream.
- Không xếp một request đã hủy hoặc hết deadline vào executor.
- Dành capacity riêng cho health check và endpoint vận hành thiết yếu.
- Áp quota hoặc fairness theo tenant để tải của một khách hàng không lan sang tất cả.
Rate limiting và backpressure có liên quan nhưng không đồng nhất. Rate limit quản lý lượng yêu cầu theo một cửa sổ hoặc danh tính, hữu ích để bảo vệ chính sách và công bằng. Backpressure phản ánh capacity tức thời của consumer. Một client nằm trong quota vẫn có thể bị từ chối khi dependency đang suy giảm; ngược lại hệ thống rảnh vẫn phải thực thi quota kinh doanh.
6. Flow control trong stream và pipeline dữ liệu
Trong pipeline, consumer phải kiểm soát tốc độ producer. Ví dụ với Node.js stream, khi write() trả về false, producer cần ngừng ghi và chờ sự kiện drain; bỏ qua tín hiệu này khiến buffer tiếp tục phình. Dùng cơ chế pipeline chuẩn của runtime thường an toàn hơn tự nối các callback vì nó phối hợp lỗi, đóng tài nguyên và flow control.
Mọi stage đều cần backpressure: đọc file, giải nén, parse, transform, ghi database và gửi mạng. Chỉ giới hạn stage cuối không đủ nếu stage giữa gom cả tập dữ liệu vào một mảng. Ưu tiên xử lý theo chunk, tránh giữ bản sao payload không cần thiết và đặt ngưỡng theo byte thay vì chỉ theo số item khi kích thước item biến động lớn.
Với giao thức có cửa sổ điều khiển luồng, vẫn không nên giả định tầng transport bảo vệ toàn bộ ứng dụng. TCP có thể làm chậm người gửi byte, nhưng không biết một message đã tốn bao nhiêu CPU hoặc query. Backpressure nghiệp vụ vẫn cần số lượng in-flight, deadline và giới hạn tài nguyên ở tầng ứng dụng.
7. Consumer message queue: prefetch, ack và backlog
Worker nên ngừng lấy thêm message khi đã dùng hết capacity. Prefetch hoặc giới hạn số message chưa được xác nhận là một hình thức backpressure: broker không giao thêm quá ngưỡng cho consumer. Ngưỡng quá cao giúp broker đẩy nhiều nhưng làm một worker giữ hàng loạt message chưa xử lý, tăng RAM, làm phân phối thiếu công bằng và khiến shutdown khó drain. Ngưỡng quá thấp có thể để worker nhàn khi độ trễ mạng đáng kể.
Chọn prefetch dựa trên concurrency thực, thời gian job, kích thước message và cách ack. Mỗi slot thường chỉ nên giữ một lượng đệm nhỏ có chủ đích. Ack chỉ sau khi hiệu ứng bắt buộc đã bền vững; nếu ack trước, crash có thể mất việc. Nếu crash sau commit nhưng trước ack, message có thể được giao lại, vì vậy handler cần idempotent.
| Tín hiệu | Ý nghĩa khả dĩ | Phản ứng cần điều tra |
|---|---|---|
| Backlog tăng, worker CPU thấp | Đang chờ dependency hoặc cấu hình fetch quá dè dặt | Đo thời gian từng dependency và trạng thái connection |
| Unacked cao, throughput không tăng | Prefetch vượt capacity hoặc job bị treo | Giảm prefetch, thêm deadline và phát hiện stuck job |
| Tuổi message tăng | Không theo kịp nhu cầu thực | Scale nếu dependency cho phép, shed tải hoặc giảm producer |
| Redelivery tăng | Crash, timeout lease hoặc ack sai thứ tự | Kiểm tra idempotency, thời gian job và shutdown |
Backlog bền vững không nhất thiết xấu nếu sản phẩm cho phép xử lý theo lô, nhưng phải có SLO về tuổi message. Đếm số message mà bỏ qua tuổi của message cũ nhất dễ che một queue ít item nhưng đã bị kẹt nhiều giờ.
8. Retry phải nằm trong cùng ngân sách tải
Khi dependency chậm, retry vô điều kiện biến một request thành nhiều request đúng lúc hệ thống ít khả năng phục vụ nhất. Backpressure chỉ hiệu quả nếu retry tôn trọng deadline, có giới hạn attempt, exponential backoff và jitter. Không retry lỗi vĩnh viễn, không bắt đầu attempt khi thời gian còn lại không đủ và không để mỗi tầng tự retry độc lập mà không có ngân sách chung.
Retry queue cũng phải hữu hạn hoặc bền vững. Nếu mọi task lỗi được giữ trong memory để thử lại, chính cơ chế phục hồi sẽ gây OOM. Với công việc bất đồng bộ, dead-letter flow cần ngưỡng attempt, lý do cuối cùng và quy trình xử lý rõ ràng. Với request đồng bộ, thường tốt hơn trả lỗi có cấu trúc để người gọi quyết định theo hợp đồng.
Circuit breaker có thể giảm lời gọi tới dependency đang lỗi, nhưng nó không thay thế concurrency limit. Trong trạng thái half-open, số request thăm dò cũng phải rất nhỏ; nếu hàng nghìn request cùng thử khi breaker mở lại, hệ thống tạo một đợt xung mới.
9. Quan sát saturation thay vì chỉ quan sát lỗi
Lỗi là tín hiệu muộn. Dashboard backpressure cần cho thấy demand, throughput và saturation cùng lúc. Đo request đến, request hoàn tất, số in-flight, queue depth, tuổi item cũ nhất, thời gian chờ, thời gian phục vụ, số lần từ chối, timeout, cancellation và mức sử dụng từng pool. Dùng histogram cho thời gian chờ và thời gian xử lý; trung bình có thể che đuôi dài.
- Queue wait / total latency: tỷ lệ tăng cho thấy nghẽn nằm trước handler.
- Pool utilization: gần 100% kéo dài cùng wait time tăng là dấu hiệu saturation.
- Rejected work: cần phân loại theo endpoint, tenant và nguyên nhân.
- Cancellation effectiveness: sau khi caller hủy, dependency có giải phóng tài nguyên thật không?
- Offered load so với completed load: khoảng cách kéo dài chính là backlog hoặc shed load.
Cảnh báo nên dựa trên SLO và xu hướng. Queue depth 1.000 có thể bình thường cho job 10 ms nhưng nghiêm trọng cho job 30 giây. Tuổi công việc và thời gian dự kiến để xả backlog thường hữu ích hơn một ngưỡng số lượng cố định.
10. Kiểm thử quá tải và chọn chính sách suy giảm
Load test không chỉ tìm requests per second tối đa. Hãy tăng tải từ từ qua điểm bão hòa và quan sát hệ thống thất bại như thế nào. Một thiết kế tốt giữ memory gần như có trần, throughput không sụp đổ đột ngột, request được từ chối sớm theo chính sách và phục hồi nhanh khi tải giảm. Một thiết kế xấu tiếp tục nhận mọi việc, latency tăng không giới hạn và vẫn bất ổn nhiều phút sau khi nguồn tải dừng.
- Đo baseline với tải ổn định và payload đại diện.
- Tạo burst ngắn để kiểm tra queue hấp thụ mà không vượt SLO.
- Duy trì offered load cao hơn capacity để xác minh rejection và trần bộ nhớ.
- Làm chậm database hoặc downstream để kiểm tra backpressure lan ngược.
- Hủy client giữa chừng và xác minh công việc phía dưới dừng thật.
- Khôi phục dependency, giảm tải và đo thời gian hệ thống trở lại bình thường.
Chính sách suy giảm phải do sản phẩm quyết định trước. Có endpoint có thể trả dữ liệu cache cũ, bỏ trường không thiết yếu hoặc chuyển công việc sang bất đồng bộ. Có endpoint tài chính phải từ chối hoàn toàn thay vì trả kết quả thiếu. Đừng để exception ngẫu nhiên trong production trở thành cơ chế ưu tiên.
Checklist triển khai
- Mọi queue trong memory có giới hạn rõ và metric độ sâu.
- Mọi pool có giới hạn concurrency dựa trên tài nguyên nghẽn thực.
- Request và job có deadline, cancellation truyền xuống dependency.
- Khi đầy, hệ thống từ chối hoặc trì hoãn theo hợp đồng đã định nghĩa.
- HTTP admission control, rate limit và quota tenant được phân biệt rõ.
- Consumer prefetch phù hợp với concurrency, kích thước message và thời gian job.
- Ack, idempotency và retry budget bảo vệ cả lỗi lặp lẫn mất việc.
- Dashboard hiển thị offered load, throughput, queue wait, saturation và rejection.
- Load test vượt điểm bão hòa xác nhận memory có trần và hệ thống phục hồi nhanh.




Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.