Lập trình · 21/09/2026

Load Shedding cho Backend: Bảo vệ hệ thống khi quá tải

Một backend quá tải không chỉ chạy chậm hơn; nó có thể bước vào vòng xoáy tự khuếch đại. Request tồn đọng giữ connection, memory và worker lâu hơn, timeout khiến client retry, retry tạo thêm tải, health check thất bại rồi lưu lượng bị dồn sang các instance còn lại. Nếu hệ thống cố nhận mọi request đến phút cuối, một đợt tăng tải ngắn có thể biến thành sự cố kéo dài.

Load Shedding cho Backend: Bảo vệ hệ thống khi quá tải

Một backend quá tải không chỉ chạy chậm hơn; nó có thể bước vào vòng xoáy tự khuếch đại. Request tồn đọng giữ connection, memory và worker lâu hơn, timeout khiến client retry, retry tạo thêm tải, health check thất bại rồi lưu lượng bị dồn sang các instance còn lại. Nếu hệ thống cố nhận mọi request đến phút cuối, một đợt tăng tải ngắn có thể biến thành sự cố kéo dài.

Load shedding là cơ chế chủ động từ chối một phần công việc khi tài nguyên tiến gần vùng nguy hiểm, nhằm giữ phần còn lại hoạt động với độ trễ có kiểm soát. Đây không phải cách thay thế capacity planning, autoscaling hay tối ưu mã. Nó là van an toàn khi nhu cầu vượt năng lực tức thời, khi dependency chậm bất thường hoặc khi một phần cụm bị mất. Bài viết trình bày cách chọn tín hiệu, đặt admission control, phân tầng ưu tiên, phối hợp retry và kiểm thử để việc từ chối có chủ đích tốt hơn việc toàn hệ thống cùng sụp đổ.

1. Quá tải là một trạng thái động, không chỉ là CPU 100%

Một dịch vụ có thể quá tải dù CPU chưa đầy. Database pool có thể hết connection; event loop có thể bị chặn; heap tiến gần giới hạn; disk queue tăng; downstream hết quota; hoặc số request đang xử lý vượt khả năng hoàn tất trước deadline. Ngược lại, CPU cao trong một khoảng ngắn chưa chắc nguy hiểm nếu hàng đợi vẫn nhỏ và latency ổn định.

Dấu hiệu quan trọng nhất thường là hệ thống không còn hoàn thành công việc nhanh bằng tốc độ nhận vào. Theo Little's Law, khi throughput hoàn tất bị giới hạn mà lượng công việc đang nằm trong hệ thống tăng, thời gian chờ sẽ tăng theo. Hàng đợi không giới hạn chỉ trì hoãn việc báo lỗi: request vẫn thất bại, nhưng thất bại muộn hơn sau khi đã tiêu tốn memory, socket và thời gian của người dùng.

Tín hiệuĐiều nó cho biếtRủi ro nếu dùng một mình
CPU utilizationÁp lực tính toán tại instanceBỏ sót nghẽn I/O, lock hoặc downstream
In-flight requestsLượng công việc đang giữ tài nguyênKhông phản ánh chi phí khác nhau giữa request
Queue depth/ageMức tồn đọng và độ cũ của công việcQueue ngắn vẫn có thể chứa tác vụ cực đắt
Latency theo percentileẢnh hưởng người dùng và dấu hiệu bão hòaLà tín hiệu trễ nếu chỉ chờ latency tăng
Memory hoặc pool usageKhoảng an toàn của tài nguyên hữu hạnNgưỡng tuyệt đối cần hiệu chỉnh theo runtime

2. Phân biệt load shedding với rate limit, backpressure và circuit breaker

Các cơ chế này liên quan nhưng bảo vệ các ranh giới khác nhau. Rate limiting áp chính sách theo client, tenant hoặc API trong một cửa sổ thời gian, thường để đảm bảo công bằng và kiểm soát quota. Backpressure làm producer chậm lại theo năng lực consumer, phù hợp với luồng có giao thức truyền phản hồi. Circuit breaker ngừng gọi một dependency đang lỗi hoặc chậm để tránh lãng phí tài nguyên. Load shedding bảo vệ chính thành phần đang nhận việc bằng cách từ chối khi áp lực thực tế vượt vùng an toàn.

Một hệ thống production thường cần phối hợp cả bốn. API gateway giới hạn tenant gây tải bất thường; consumer điều tiết tốc độ đọc; client có circuit breaker với downstream; còn mỗi instance vẫn có admission control cục bộ. Chỉ dựa vào gateway là chưa đủ vì phân phối tải không hoàn hảo, chi phí request không đồng đều và một instance có thể gặp vấn đề riêng.

Load shedding trả lời câu hỏi: “Với trạng thái tài nguyên ngay lúc này, request nào còn đáng được nhận vào để hệ thống hoàn thành nhiều công việc hữu ích nhất?”

3. Đặt admission control trước tài nguyên khan hiếm

Điểm từ chối nên nằm trước bước đắt đỏ. Nếu request đã parse payload lớn, mở transaction, lấy connection database và gọi hai dịch vụ khác rồi mới kiểm tra quá tải, hệ thống đã trả phần lớn chi phí. Admission control nên chạy sau các kiểm tra tối thiểu cần thiết để xác định danh tính, loại request và mức ưu tiên, nhưng trước khi cấp tài nguyên khan hiếm.

function handle(request):
  context = authenticateCheaply(request)
  class = classify(context, request.route)

  if deadlineAlreadyExpired(request):
    return error(408, "deadline expired")

  permit = admission.tryAcquire(class, estimatedCost(request))
  if permit is null:
    return overloadResponse(class)

  try:
    return executeBusinessWork(request)
  finally:
    permit.release()

Permit phải được giải phóng trong mọi đường đi, kể cả exception và client disconnect. Nếu có nhiều tầng tài nguyên, hãy xác định thứ tự lấy permit thống nhất để tránh deadlock. Không giữ permit trong lúc chờ một công việc không còn giá trị: khi deadline hết hoặc client đóng kết nối, cần hủy downstream call nếu giao thức hỗ trợ.

4. Giới hạn concurrency thường sát thực tế hơn giới hạn QPS

QPS dễ đo nhưng giả định request có chi phí gần nhau. Một endpoint đọc cache có thể hoàn tất trong vài mili giây, trong khi export báo cáo giữ CPU và database hàng giây. Cùng 100 QPS có thể tạo mức tải hoàn toàn khác. Giới hạn số công việc đồng thời gắn trực tiếp hơn với số worker, connection hoặc memory mà dịch vụ đang giữ.

Có thể tách semaphore theo loại công việc: interactive read, write, batch và admin. Một pool chung quá nhỏ làm batch chặn request người dùng; một pool tách tuyệt đối lại để tài nguyên nhàn rỗi không được chia sẻ. Mô hình thực dụng là có giới hạn riêng cho từng lớp cộng với trần toàn cục. Tác vụ nặng có thể tiêu nhiều “đơn vị permit” dựa trên ước lượng chi phí thay vì mọi request đều tính là một.

  • Đặt giới hạn ban đầu từ load test và số tài nguyên hữu hạn, không đoán theo cảm giác.
  • Theo dõi thời gian giữ permit, tỷ lệ từ chối và throughput hoàn tất.
  • Dành headroom cho health check, telemetry và thao tác khôi phục.
  • Không tự động tăng concurrency chỉ vì queue tăng; đó có thể là dấu hiệu cần giảm nhận việc.

5. Hàng đợi phải hữu hạn và tôn trọng deadline

Queue giúp hấp thụ burst ngắn khi năng lực trung bình vẫn đủ. Nhưng queue dài biến tải thành latency và che khuất bão hòa. Mỗi queue cần giới hạn số phần tử hoặc tổng byte, timeout chờ và metric về tuổi của phần tử lâu nhất. Khi không còn khả năng hoàn thành trước deadline, loại request sớm tốt hơn để nó chiếm chỗ.

Giả sử request có deadline còn 200 ms nhưng tuổi queue hiện tại đã 300 ms. Đưa nó vào cuối hàng chỉ tạo công việc vô ích. Admission controller có thể dùng budget còn lại, ước lượng service time và độ sâu queue để quyết định. Với job bất đồng bộ, deadline có thể là thời điểm hết giá trị kinh doanh, chẳng hạn thông báo không cần gửi sau khi chiến dịch kết thúc.

remaining = request.deadline - now()
predicted = queue.waitEstimate() + serviceTimeEstimate(request.class)

if remaining <= predicted:
  reject("cannot finish before deadline")
if queue.isFull():
  reject("queue capacity exhausted")

Ước lượng không cần hoàn hảo để hữu ích, nhưng phải bảo thủ và quan sát được. Nếu workload có đuôi dài, dùng percentile phù hợp thay vì trung bình. Queue cũng nên tách theo priority để một lượng lớn tác vụ nền không chiếm hết chỗ của request tương tác.

6. Shed theo mức ưu tiên và giá trị kinh doanh

Từ chối ngẫu nhiên đơn giản nhưng có thể loại health-critical write trong khi vẫn phục vụ ảnh trang trí. Hãy định nghĩa một số ít lớp ưu tiên rõ ràng: control plane/khôi phục, giao dịch cốt lõi, request tương tác thông thường, tính năng tùy chọn và batch. Mỗi lớp có ngưỡng, quota tối thiểu hoặc concurrency pool tương ứng.

LớpVí dụHành vi khi áp lực tăng
Khôi phụcHealth, leader lease, thao tác giảm tảiGiữ headroom, payload nhỏ, dependency tối thiểu
Cốt lõiThanh toán đã xác nhận, ghi trạng thái quan trọngƯu tiên permit nhưng vẫn có trần an toàn
Tương tácTrang sản phẩm, tìm kiếm, API người dùngGiảm chất lượng hoặc từ chối có kiểm soát
Tùy chọnGợi ý, ảnh phụ, enrichmentTắt sớm khi chạm ngưỡng mềm
NềnExport, backfill, đồng bộ định kỳTạm dừng và tiếp tục sau

Priority do client tự khai báo là không đáng tin. Server phải suy ra từ route, danh tính đã xác thực và policy được quản lý. Cần ngăn starvation: lớp cao cũng có quota tối đa, còn lớp thấp có phần năng lực tối thiểu nếu nghiệp vụ yêu cầu tiến triển. Policy phải đủ đơn giản để đội trực vận hành hiểu được trong sự cố.

7. Graceful degradation giữ giá trị với chi phí thấp hơn

Không phải request nào cũng chỉ có hai kết quả đầy đủ hoặc lỗi. Khi chạm ngưỡng mềm, dịch vụ có thể bỏ enrichment, dùng cache hơi cũ, giảm số kết quả, chuyển thuật toán sang biến thể rẻ hơn hoặc trả dữ liệu đã tính trước. Đây là graceful degradation: giảm chất lượng có chủ đích để duy trì chức năng cốt lõi.

Mỗi fallback phải thực sự rẻ và độc lập hơn đường chính. Gọi thêm một dịch vụ “dự phòng” dùng chung database không làm giảm tải; nó có thể nhân đôi công việc. Cache fallback cần giới hạn độ cũ và thể hiện rõ semantics. Với thao tác ghi, không được báo thành công nếu dữ liệu chưa được cam kết; có thể nhận vào hàng đợi bền vững nếu contract cho phép, hoặc trả lỗi có thể retry.

  • Thiết kế degradation mode trước sự cố và kiểm thử thường xuyên.
  • Gắn metric để biết tỷ lệ response đang ở chế độ giảm chất lượng.
  • Tránh fallback đệ quy giữa các dịch vụ.
  • Tự động phục hồi có hysteresis để không bật/tắt liên tục quanh một ngưỡng.

8. Phản hồi quá tải phải hướng dẫn client đúng cách

Với HTTP, 429 Too Many Requests thường phù hợp khi một client hoặc quota cụ thể bị giới hạn; 503 Service Unavailable phù hợp hơn khi dịch vụ tạm thời không đủ năng lực. Retry-After có thể hữu ích nếu server biết thời gian hợp lý, nhưng không nên đưa ra một con số giả chính xác. API nội bộ có thể dùng mã lỗi riêng như OVERLOADED kèm trường retryable.

Client không được retry tức thì vô hạn. Retry phải có exponential backoff, jitter, deadline tổng và retry budget. Chỉ retry thao tác idempotent hoặc thao tác có idempotency key đúng. Khi cả cụm quá tải, retry khuếch đại lưu lượng; khi chỉ một instance bão hòa, một lần retry có giới hạn sang instance khác có thể hữu ích. Load balancer và client cần phân biệt hai trường hợp qua telemetry thay vì đoán.

if response.isOverloadError():
  if !request.isSafeToRetry() or budget.exhausted():
    return response
  delay = min(backoff.nextWithJitter(), request.remainingDeadline())
  if delay <= 0:
    return response
  wait(delay)
  return retryOnEligibleTarget()

9. Ngưỡng cần hysteresis và nhiều cấp hành động

Một công tắc duy nhất tại 90% dễ dao động: vừa shed tải thì utilization giảm, hệ thống mở lại, tải tràn vào và lại đóng. Hysteresis dùng ngưỡng vào cao hơn ngưỡng thoát, đồng thời yêu cầu trạng thái ổn định trong một khoảng thời gian. Có thể triển khai nhiều cấp thay vì chuyển thẳng từ bình thường sang từ chối tất cả.

  1. Bình thường: nhận việc theo quota và ghi baseline.
  2. Áp lực nhẹ: tắt tính năng tùy chọn, giảm prefetch và batch concurrency.
  3. Áp lực cao: shed lớp thấp, rút ngắn queue, ưu tiên request có deadline khả thi.
  4. Khẩn cấp: chỉ giữ control traffic và thao tác cốt lõi trong giới hạn cứng.

Ngưỡng không nên phụ thuộc vào một metric nhiễu. Có thể kết hợp memory pressure cứng với in-flight và queue age, nhưng logic cần xác định rõ precedence. Một giới hạn hard safety không được override bởi tín hiệu trung bình toàn cụm nếu instance cục bộ đang gần OOM.

10. Quan sát đúng tỷ lệ từ chối và công việc hữu ích

Dashboard chỉ hiển thị error rate sẽ khiến load shedding trông như làm hệ thống tệ hơn, dù nó đang ngăn outage toàn phần. Cần tách lỗi do overload chủ động khỏi exception và dependency failure, đồng thời đo số request hoàn tất trong SLO. Mục tiêu không phải zero rejection trong mọi tải, mà là tối đa hóa công việc hữu ích và giữ failure có giới hạn.

  • Accepted, queued, shed và completed theo route, tenant class và priority.
  • Queue depth, queue age, in-flight, permit wait và permit hold time.
  • CPU, memory, connection pool, thread/event-loop saturation và downstream latency.
  • Retry rate, retry success, request amplification và deadline-expired work.
  • Tỷ lệ degraded response, fallback latency và độ cũ dữ liệu fallback.

Log mỗi request bị shed có thể tự tạo overload I/O. Hãy dùng counter, histogram, sampling và log gộp. Trace nên ghi quyết định admission và pressure snapshot với cardinality được kiểm soát. Cảnh báo phải dựa trên cả tỷ lệ shed và thời gian kéo dài; một burst vài giây khác với thiếu capacity liên tục.

11. Kiểm thử vượt điểm bão hòa và kiểm tra khả năng phục hồi

Load test thường dừng khi đạt throughput mục tiêu, trong khi cơ chế này chỉ được chứng minh khi vượt mục tiêu. Tăng tải theo bậc qua điểm bão hòa, giữ đủ lâu để queue và autoscaler phản ứng, rồi giảm tải để quan sát phục hồi. Một thiết kế tốt có throughput hữu ích đạt plateau, latency của phần được nhận vẫn có giới hạn, tỷ lệ shed tăng có chủ đích và hệ thống hồi phục mà không cần restart.

Test thêm workload hỗn hợp: request rẻ và đắt, một tenant lớn, dependency chậm, mất một phần instance, pool database co lại và retry storm. Xác nhận lớp ưu tiên cao thật sự còn headroom. Fault injection phải có guardrail và thực hiện ở môi trường phù hợp trước khi canary production.

assert completed_throughput does not collapse after saturation
assert accepted_request_latency remains within overload objective
assert queue_age is bounded
assert low_priority sheds before critical traffic
assert retry_amplification stays within budget
assert service returns to normal after offered load drops

12. Quy trình triển khai an toàn

  1. Đo bottleneck thật và xây load test tái lập được.
  2. Thêm metric admission ở chế độ quan sát, chưa từ chối, để đánh giá policy.
  3. Đặt queue và concurrency limit bảo thủ cho một route ít rủi ro.
  4. Canary một phần instance; so sánh completed throughput, latency và lỗi.
  5. Thêm priority và degradation từng bước, không triển khai nhiều policy cùng lúc.
  6. Viết runbook gồm cách giảm ngưỡng, tắt policy, nhận biết false positive và rollback.
  7. Chạy game day định kỳ để kiểm tra retry, autoscaling và dashboard vẫn phối hợp đúng.

Load shedding thành công khi người dùng thấy một phần request thất bại nhanh và có thể dự đoán thay vì tất cả request treo rồi thất bại. Nó cần được thiết kế cùng deadline, retry, idempotency, queue và capacity planning. Van an toàn không làm động cơ mạnh hơn, nhưng giúp động cơ không tự phá hủy khi tải vượt giới hạn.

Tài liệu tham khảo

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

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