Backpressure: cum oprești cascada când downstream nu mai face față
Backpressure este semnalul prin care un sistem spune că nu mai poate procesa mai mult input. Strategii: drop, block, buffer cu limit, sample. Cum monitorizezi.
Cuprins
Backpressure este semnalul prin care o componentă a unui sistem îi spune producătorului din amonte că nu mai poate procesa mai mult input. Apare oriunde un producător generează date mai rapid decât un consumator le poate prelucra: coada crește, latența crește, iar dacă nimeni nu reacționează, procesul se termină cu OOM sau cu un restart forțat.
Termenul vine din ingineria fluidelor, unde descrie rezistența unui fluid față de curgerea în sens opus. În software, sensul este același: forța care frânează un producător atunci când aval nu mai există capacitate liberă. Confuzia frecventă este că backpressure ar fi o problemă care apare doar în sisteme distribuite mari. Nu este. Apare în orice pipeline unde viteza producătorului și viteza consumatorului pot diverge, inclusiv în cel mai simplu serviciu HTTP cu un pool de fire de execuție.
Ce înseamnă backpressure mai exact?
O proprietate a legăturii dintre un producător și un consumator, nu o tehnologie sau un pattern de arhitectură. Definiția operațională: sistemul are backpressure dacă producătorul primește un semnal explicit sau implicit că trebuie să reducă ritmul sau să oprească temporar generarea de date noi.
Semnalul poate fi:
- Explicit: consumatorul returnează un cod de eroare sau un mesaj structurat care spune „nu mai pot prelua nimic acum". Protocoale ca reactive streams (standardul care stă la baza RxJava și Project Reactor) formalizează acest semnal ca parte din specificație.
- Implicit: coada dintre producător și consumator se umple. Dacă producătorul face blocking write, se oprește singur. Dacă nu, coada crește până la epuizarea memoriei.
Diferența față de rate limiting este că rate limiting-ul controlează accesul clienților externi la sistem, în timp ce backpressure controlează ce se întâmplă înăuntrul sistemului, între componentele sale.
Cum se manifestă în pipeline-uri reale?
Manifestarea urmează de obicei aceeași secvență: coada crește, latența crește, procesul cade.
Coada crește când consumatorul nu mai procesează la ritmul producătorului. Dacă monitorizezi queue depth (numărul de mesaje sau sarcini în așteptare), îl vei vedea crescând constant, nu în pulsuri. O coadă care crește liniar e un semn că sistemul nu e în echilibru: capacitatea consumatorului e mai mică decât debitul producătorului.
Latența crește imediat după ce coada depășește un prag. Fiecare element nou trebuie să aștepte tot ce era deja în coadă. La o coadă de 1.000 de elemente cu un timp mediu de procesare de 100 ms per element, ultimul element intrat așteaptă 100 de secunde înainte să fie procesat. Utilizatorul care trimite cererea nu știe asta; el vede doar că aplicația este lentă.
OOM-ul sau restart-ul vine dacă backpressure-ul este ignorat complet. Producătorul continuă, coada crește, memoria se umple. Procesul este ucis de kernel sau se oprește cu OutOfMemoryError. La restart, coada este pierdută dacă nu e persistentă; ciclul poate reîncepe imediat dacă cauza nu a fost înlăturată. Legătura cu trasarea distribuită este directă: un serviciu care cade repetat din cauza backpressure-ului va produce spans întrerupte și corelații de cauzalitate greu de reconstituit.
Care sunt strategiile de gestionare a backpressure-ului?
Există patru strategii fundamentale, fiecare cu trade-off-uri clare între pierdere de date, latență și complexitate de implementare:
- Drop. Cererile sau mesajele care depășesc capacitatea sunt eliminate. Producătorul primește o eroare sau o confirmare falsă că mesajul a fost primit, dar acesta nu mai ajunge la consumator. Avantaj: latența rămâne scăzută și memoria sub control. Dezavantaj: pierdere de date. Potrivit pentru metrici de telemetrie sau evenimente de logging unde câteva eșantioane pierdute nu schimbă concluzia.
- Block. Producătorul este suspendat până când consumatorul eliberează capacitate. Nu există pierdere de date, dar latența crește proporțional cu cât timp consumatorul are nevoie. Potrivit când fiecare eveniment trebuie procesat (tranzacții, comenzi, modificări de stare). Riscul este că blocarea se propagă în amonte: dacă blocul durează prea mult, și producătorul blocului anterior se oprește, creând un lanț de așteptare.
- Buffer cu limită. Se acceptă un număr limitat de mesaje în așteptare. Când bufferul este plin, se aplică drop sau block, la alegere. Avantaj: absoarbe spike-uri scurte fără impact vizibil. Dezavantaj: dacă spike-ul durează mai mult decât poate absorbi bufferul, te afli din nou în situația anterioară. Dimensionarea bufferului nu e o decizie de o singură dată: se ajustează pe baza pattern-urilor de trafic observate în producție, nu a estimărilor din design.
- Sample. Procesează un subset reprezentativ al datelor, renunțând la restul într-un mod controlat statistic. Potrivit pentru traces sau metrici la volum foarte mare, unde 1% din eșantioane oferă o imagine statistică validă. Nu potrivit pentru date care trebuie să fie complete (facturi, audit logs, comenzi). Diferența față de drop este că sample-ul este deliberat și poate fi configurat; drop-ul apare ad-hoc când sistemul nu mai face față.
Nicio strategie nu este universal corectă. Alegerea depinde de ce înseamnă pierderea unui eveniment pentru cazul tău concret. Aceasta este conexiunea cu error budget-ul unui serviciu: cât risc de pierdere sau de latență ești dispus să accepți și în ce condiții.
Cum se leagă backpressure de rate limiting?
Rate limiting-ul aplicat la marginea sistemului este backpressure-ul cel mai explicit posibil: refuzi cereri noi înainte ca bufferele interne să se umple. Pe PromoAzi, rate limit-ul aplicat per IP (120 cereri/minut general, 30/minut pe endpoint-urile costisitoare ca history și agregări) este forma server-side a backpressure-ului: refuzăm explicit cu 429 când clientul depășește pragul, în loc să acumulăm cererile lui într-o coadă care apoi consumă RAM.1 Clientul primește un semnal clar; sistemul intern rămâne stabil. Detaliile de implementare sunt descrise în intrarea despre rate limiting.
Conexiunea inversă este la fel de importantă: un sistem intern care nu gestionează backpressure-ul face rate limiting-ul ineficient. Dacă procesezi cererile admise mai lent decât sosesc, coada internă crește chiar și sub limita de rate. Rate limiting-ul controlează intrarea, backpressure-ul controlează procesarea. Ai nevoie de ambele.
O altă formă de backpressure la marginea sistemului este circuit breaker-ul: când un serviciu din aval devine lent sau indisponibil, circuit breaker-ul întrerupe imediat fluxul, fără să aștepte ca cozile să se umple.
Cum o vedem în observabilitate?
Backpressure-ul nu generează o eroare cu text explicit. Se citește din combinația a trei tipuri de semnale, în ordinea în care apar:
Queue depth este primul semnal. O coadă care crește constant, fără să se golească, arată dezechilibrul înainte ca utilizatorii să observe ceva. Dacă folosești un sistem de mesagerie sau o coadă de sarcini, exportarea acestei metrici în stack-ul tău de observabilitate este obligatorie, nu opțională.
Latența end-to-end este al doilea semnal. Crește cu câteva secunde sau zeci de secunde înainte ca procesele să cadă. Pe graficele de percentile (p95, p99), latența ridicată apare mai devreme și mai clar decât pe medie. Un grafic de medie poate masca faptul că 1% dintre cereri așteaptă de zece ori mai mult decât restul.
Restart-urile procesului sunt ultimul semnal. Dacă un proces se repornește la fiecare câteva ore, motivul cel mai probabil este fie un memory leak, fie backpressure-ul netratat care umple memoria. Numărul de restart-uri per zi este o metrică simplă de adăugat la orice dashboard. Combinată cu trasarea distribuită, permite reconstituirea exactă a ce s-a întâmplat înainte de fiecare restart.
Ordinea contează: vrei să acționezi la primul semnal (queue depth), nu la al treilea (restart). Un sistem care reacționează la backpressure doar după ce procesul cade nu are backpressure management, are restart management. Diferența o simți în numărul de nopți în care ești trezit.
- PromoAzi aplică rate limiting per IP folosind Bucket4j (in-memory, fără Redis), cu două praguri: general (120 cereri/minut, 2000/oră) și strict pentru endpoint-urile costisitoare (30/minut, 300/oră). Identificarea trece prin
CF-Connecting-IP.[promoazi.rate_limit_tiers]
Întrebări frecvente
Ce diferență este între backpressure și rate limiting?
Rate limiting-ul controlează câți clienți externi pot accesa sistemul; backpressure controlează ce se întâmplă când componentele interne nu mai țin ritmul una cu alta. Rate limiting-ul se aplică la marginea sistemului, între client și server. Backpressure apare înăuntru: producătorul intern generează mai rapid decât consumatorul intern poate procesa. Efectul final este similar (cereri respinse sau întârziate), dar cauzele și soluțiile sunt diferite.
Când aleg drop față de block ca strategie?
Drop când pierderea câtorva evenimente este acceptabilă și vrei să menții latența; block când fiecare eveniment contează și ești pregătit să accepți latență mai mare. Un pipeline de telemetrie poate pierde 0,1% din metrici fără consecințe. Un pipeline de tranzacții financiare nu poate pierde nimic, deci block este obligatoriu, iar capacitatea consumatorului trebuie dimensionată corespunzător.
Backpressure există și în HTTP simplu, nu doar în streaming?
Da: un server HTTP care returnează 429 sau 503 aplică backpressure față de clienții săi. Diferența față de sistemele de streaming este că în HTTP backpressure-ul este explicit (un cod de status) și asincron (clientul reîncearcă când vrea). În streaming reactiv sau în cozi de mesaje, backpressure-ul este parte din protocolul de transport și producătorul este oprit în timp real.
Queue depth este singura metrică relevantă pentru backpressure?
Este prima și cea mai directă, dar nu singura. Queue depth crește vizibil înainte ca latența să se degradeze grav. Latența end-to-end este a doua, deoarece arată impactul asupra utilizatorului. Rata de erori și restart-urile procesului sunt a treia, deoarece marchează momentul în care sistemul a cedat. Cele trei împreună formează un tablou complet.
RxJava sau Reactor gestionează backpressure automat?
Oferă primitivi pentru backpressure, dar nu îl rezolvă automat. RxJava 2+ și Project Reactor expun operatori ca onBackpressureBuffer, onBackpressureDrop și onBackpressureLatest care codifică explicit una dintre strategii. Dacă nu specifici o strategie și producătorul e mai rapid decât consumatorul, primești MissingBackpressureException (RxJava) sau overflow silențios (Reactor fără buffer limit). Primitivi excelenti, dar decizia de strategie rămâne a ta.