- Sustained load thì autoscaler lo được.
- Nhưng khi burst đột ngột kèm payload lớn, heap nổ trước khi pod mới kịp lên.
- Giải pháp: một pressure valve — khi heap tăng cao, worker ngừng poll việc mới cho tới khi thở được.
TL;DR
Enes Akar (CEO Upstash) vừa nêu một quan sát đơn giản nhưng dễ bị quên: sustained load không phải kẻ thù — autoscaler xử lý được. Kẻ thù thật sự là burst đột ngột + payload lớn. Heap sẽ nổ trước khi scaler kịp phản ứng. Câu trả lời team của ông chọn: một memory-aware pressure valve — khi heap usage vọt lên, worker tự động ngừng poll việc mới cho tới khi bộ nhớ dịu lại. Nguyên tắc gốc: Backpressure > Crashing.
Điểm mới trong góc nhìn này
Hầu hết tài liệu về scaling đều nói về RPS, CPU, queue depth. Nhưng ba metric đó đều bỏ sót case payload lớn. Một worker có thể đang xử lý chỉ 3 job/giây và CPU chỉ 20%, nhưng nếu mỗi job là một PDF 80MB hoặc một batch prompt với ảnh đính kèm, heap vẫn có thể leo thẳng lên 90% trong vài giây.
Pressure valve đảo lại bài toán: thay vì đo "bao nhiêu việc", ta đo "bao nhiêu byte đang nằm trong process". Và thay vì chờ autoscaler (mất nhiều giây đến nhiều phút để boot instance mới), ta phản ứng ngay trong process bằng cách bỏ qua lời gọi poll kế tiếp. Đơn giản, rẻ, và hoạt động trên mọi pull-based consumer: Kafka, SQS, QStash, BullMQ, Celery.
Vì sao nó quan trọng
2026 là năm workload AI đẩy độ biến thiên payload lên mức kỷ lục. Một endpoint duy nhất có thể nhận prompt 500 byte trong request này và một bundle PDF + ảnh 120MB ở request kế. Autoscaler dựa trên CPU/RPS đơn giản là không đủ nhanh và không đủ chính xác cho case này.
Ngoài ra, với kiến trúc multi-tenant, một khách hàng bulk upload không nên kéo sập worker của các khách hàng khác. Một van áp suất per-worker là lớp cách ly rẻ tiền nhất trước khi bạn phải đi thiết kế hẳn tenant isolation ở tầng hạ tầng.
Chi tiết kỹ thuật
Mẫu triển khai chuẩn (đã có trong OpenTelemetry Collector dưới tên memory_limiter) gồm hai ngưỡng:
| Ngưỡng | Hành vi | Ví dụ (hard 1024 MiB, spike 256 MiB) |
|---|---|---|
| Soft limit | Bật backpressure: dừng poll / báo upstream slow down | Kích hoạt ở 768 MiB |
| Hard limit | Drop data để tránh OOM crash | Kích hoạt ở 1024 MiB |
Tần suất đo heap thường 1 giây một lần (check_interval). Trên Node.js, process.memoryUsage().heapUsed rẻ và có thể gọi thường xuyên. Trên JVM, phí đo cao hơn và còn phụ thuộc GC — heap graph có thể vẫn cao sau khi GC vừa chạy xong, nên quyết định dựa trên mẫu liên tiếp (sustained), đừng dựa trên một sample đơn lẻ.
Về sizing queue, một heuristic đã được OpenTelemetry dùng: batches_per_second × buffer_duration = queue_size. Bản "memory-safe" của công thức là tính theo byte budget, không phải số item. Lý do đúng chính là lý do Akar nêu: item-count không nói cho bạn biết một item nặng bao nhiêu.
So sánh với các chiến lược backpressure khác
| Chiến lược | Kích hoạt dựa trên | Điểm yếu với burst + payload lớn |
|---|---|---|
| Memory-aware pressure valve | Heap usage | Phụ thuộc độ chính xác heap reading, quirk GC |
| Queue-depth throttling | Độ dài queue | Vài item lớn vẫn đủ OOM khi queue "ngắn" |
HTTP 429 + Retry-After | Rate limit ở ingress | Cần upstream hợp tác; không bảo vệ heap nội bộ |
| Circuit breaker | Lỗi downstream | Phản ứng sau khi đã hỏng |
| Token bucket / adaptive pacing | Rate trung bình | Smooth average; burst payload lớn vẫn vào |
| Chỉ autoscaling | CPU / RPS | Quá chậm cho burst; không bảo vệ instance đơn |
Kết luận gọn: các chiến lược đếm (count/rate) đều bỏ sót case payload lớn. Chỉ một van byte-aware / heap-aware mới bắt được.
Ứng dụng thực tế
- Job queue workers: QStash, BullMQ, SQS consumer, Sidekiq, Celery, Temporal — bất kỳ worker nào xử lý job với payload không đồng đều.
- Streaming pipelines: Kafka consumer cho message biến thiên size, OpenTelemetry collector trong observability pipeline.
- AI / LLM inference workers: prompt nhỏ, nhưng attachment (ảnh, PDF) tạo heap spike lớn trên mỗi request — case kinh điển cho valve này.
- File processors: transcoder video, resizer ảnh, parser tài liệu.
- Multi-tenant SaaS: ngăn một tenant bulk upload kéo sập worker chung.
Không cần với API CPU-bound, payload bé (REST JSON dưới 100KB) — autoscaler xử lý đủ tốt.
Giới hạn & lưu ý
- Measurement overhead: đo heap quá dày ảnh hưởng throughput. Trên JVM cân nhắc kỹ; trên V8/Node khá rẻ.
- GC lag: heap usage là leading indicator nhưng bị nhiễu bởi GC. Dùng moving window, không dùng sample đơn lẻ. Bài "Node backpressure myths" của ThinkingLoop có nhắc: "heap graphs look scary khá trễ so với lúc áp suất thực sự bắt đầu".
- Starvation risk: nếu valve đóng quá lâu, queue broker phình ra không giới hạn. Luôn kết hợp với TTL, DLQ, hoặc 429 ở producer.
- Không thay thế bounded queue: đây là lớp cuối cùng, không phải lớp duy nhất. Algoroq nói thẳng: "An unbounded queue is a silent promise to process all work eventually — a promise the system cannot keep under sustained overload."
- Không áp dụng trực tiếp cho push ingress (HTTP/gRPC stream) — những nơi đó cần shedding ở ingress.
Bước tiếp theo cho team bạn
Nếu bạn đang chạy worker pool, hãy làm bốn việc theo thứ tự:
- Đặt bounded queue ở mọi tầng. Unbounded queue là bom nổ chậm.
- Instrument
heapUsed / heap_size_limitvà log nó như một first-class metric. - Đặt soft threshold 70–75%, hard threshold 90%. Ở soft, bỏ qua
poll()kế tiếp. Ở hard, từ chối job đang cầm và ném về DLQ. - Chuyển hạn mức từ
maxInFlightJobssangmaxInFlightByteskhi SDK hỗ trợ — 2026 các queue SDK sẽ dần expose tham số này.
Câu tagline của Akar đáng dán lên tường phòng platform: Backpressure > Crashing. Thà chậm lại, hơn là sập.
Nguồn: Enes Akar trên X, OpenTelemetry memory_limiter patterns, Algoroq — Backpressure Patterns.