TSM - Servicii IA auto-sustenabile: de la capacitate concurentă alocată la capacitate variabilă

Crina Boitor - Java & Cloud DevOps Engineer @ BMW TechWorks Romania

Modelele generative de limbaj (LLM) au dat complet peste cap profilul de latență al serviciilor de tip enterprise. Un request care înainte aștepta 40 ms pentru acces I/O, acum este blocat timp de 20 de secunde pentru a primi răspunsul de la un model. În arhitectura clasică care implică un thread-per-request, această realitate transformă concurența într-o constantă rigidă, care trebuie determinată în avans. Rezultatul? Un pod saturat raportează un consum CPU aproape de zero, deși nu mai poate primi trafic adițional. Decizii arhitecturale calibrate pentru zeci de milisecunde au fost determinate pentru un status-quo care nu mai corespunde realității.

În acest articol, urmărim să măsurăm trei modele de execuție ale unui software Java simplu având la bază frameworkul Quarkus. Un pool de 200 de threaduri, la o calibrare corect adaptată, se blochează complet la doar 9.96 requesturi/sec (cu o precizie de 0.3% comparativ cu aproximarea în baza Legii lui Little) și are nevoie de 51 de poduri acolo unde utilizarea modelului reactiv sau a unuia bazat pe threaduri virtuale implică un singur pod.

Cele două abordări non-blocking sunt identice ca latență. Totuși, modelul reactiv consumă ceva mai puține resurse, ceea ce înseamnă că alegerea threadurilor virtuale se justifică mai degrabă prin simplitatea codului și reducerea costului de dezvoltare. În plus, adaptarea limitei de concurență la nivelul unui semafor în cadrul procesului permite preluarea de încărcare din upstream prin degradarea controlată a calității modelului. Astfel, păstrăm 100% din throughput, în timp ce o strategie clasică simulată de scalare orizontală (HPA) pierde 58% din trafic și rămâne complet blocată timp de 35 de secunde.

Să ne imaginăm un pod care rulează la 3% CPU, dar totuși nu mai poate accepta niciun request.

Pare neverosimil. Și cu toate acestea procesorul este inactiv pentru că execuția în sine este suspendată/blocată: fiecare thread este blocat la citire, așteptând ca un LLM să termine de generat tokenuri. Podul este, în același timp, complet saturat și total inactiv.

Un endpoint REST clasic, care comunică cu o bază de date, așteaptă în medie 40 ms per I/O. Un apel către un LLM durează între 2 și 60 de secunde, iar arhitecturile de tip RAG și pipeline-urile diferitelor modele agentice folosesc între trei și zece astfel de apeluri înlănțuite pentru un singur request din partea utilizatorului. Latența a crescut astfel exponențial, trecând la alt ordin de mărime.

Dacă aplicăm Legea lui Little [1] (L = λ × W), putem vedea numărul de threaduri de sistem pe care podul trebuie să le țină active simultan.

Tip serviciu λ (requesturi) W (Latență) L (threaduri în execuție)
REST clasic + bază de date 500 0.04 s 20
RAG / Aplicații LLM 500 20 s 10 000

Tabelul 1 - Același trafic, la câteva ordine de mărime distanță

Traficul este identic, s-a schimbat doar serviciul pe care îl apelăm. Problema este că pentru 10.000 de threaduri de sistem de operare, la aproximativ 1 MB per thread, am avea nevoie de aproximativ 10 GB de RAM doar pentru aceasta, în cazul fiecărui pod. Astfel că serviciul este limitat artificial (de exemplu, la un pool de 200 de threaduri), în timp ce requesturile așteaptă sau sunt respinse direct.

Problema esențială este că acest blocaj rămâne complet invizibil pentru marea majoritate a mecanismelor de auto-scalare. Threadurile blocate nu consumă CPU, astfel că podul raportează un load de câteva procente, deși nu mai poate primi niciun request adițional. Prin urmare, mecanismul de auto-scalare orizontală reacționează chiar prin sub-alocarea de resurse în momentul în care serviciul este cel mai solicitat - metrica evoluează în direcția greșită relativ la limitarea impusă. Echipele de dezvoltare ajung astfel să compenseze prin supra-alocarea de resurse, sau prin adaptarea mecanismului de auto-scalare, ceea ce reintroduce la nivelul infrastructurii o decizie pe care aplicația putea să o facă deja în avans.

Pentru comparația de față am folosit un pool de threaduri setat la 200, pentru a simula un proces de producție. Numărul a fost ales doar pentru a susține ideea că o configurare aleasă înainte de deploy introduce o limitare ce poate fi depășită prin intermediul unui proces de auto-guvernanță. În prezent, concurența este definită rigid după formula număr poduri × dimensiune pool, ambele fiind fixate înainte ca procesul să pornească. Practic, nu există nicio posibilitate de control în cadrul procesului.

Propunem astfel un proces de auto-guvernanță în cadrul procesului, prin care serviciul rulează un ciclu autonom de control asupra consumului de resurse propriu, în trei etape distincte.

Pentru etapa de detecție se iau în considerare patru variabile disponibile în cadrul procesului: numărul de permise la nivelul semaforului care gestionează concurența, latența medie raportată de Micrometer, rata de răspunsuri cu status 429 primite de la furnizorul modelului și numărul de tokenuri utilizate per oră raportat la bugetul alocat.

Pasul decizional utilizat este deliberat simplist și anume: în cazul în care numărul de permise la nivel de semafor a fost epuizat sau dacă numărul de tokenuri utilizate depășește bugetul, se trece la utilizarea unui model de nivel scăzut. Urmează etapa de auto-guvernanță, în ordinea priorității: modificarea în timp real a numărului de permise la nivel de semafor, nivelul de direcționare a traficului către un model mai sofisticat sau mai simplist, iar ca ultimă soluție, răspunsul cu statusul 503, doar dacă primele două configurări nu au efect.

Ordinea de execuție este esențială, dat fiind că un serviciu clasic blocking de tip thread-per-request poate aplica doar direct pasul 3 (503), deoarece pașii 1 și 2 necesită o etapă decizională care în acest caz s-a luat deja, când s-a blocat orice request adițional.

Threadurile virtuale nu accelerează inferența. Nimic din acest mecanism nu optimizează viteza unui forward pass.

Modelul reactiv este mai eficient din punctul de vedere al resurselor, iar cifrele o demonstrează. La o rată a fluxului λ=500, varianta reactivă a consumat cu 13% mai puțină memorie (556 vs. 631 MB) și cu 20% mai puțin CPU (35% vs. 42%) comparativ cu threadurile virtuale, latența Micrometer p99 având o diferență de doar 0,1 ms de-a lungul a 30.001 eșantioane. La λ=1.000, diferența crește: cu 9% mai puțină memorie și cu 41% mai puțin CPU.

Oricine urmărește exclusiv optimizarea resurselor pentru acest tip de problemă ar trebui să aleagă modelul reactiv. Argumentul în favoarea threadurilor virtuale constă în faptul că ciclul de guvernanță descris se reduce la o simplă ramură decizională într-un cod de proces liniar; fiind o paradigmă axată pe costul de dezvoltare și depanare, nu pe maximizarea traficului de vârf.

Alegerea dintre threaduri virtuale și programarea reactivă în pipeline-urile generative și arhitecturile de tip RAG nu este o dispută privind performanța brută a algoritmilor de inteligență artificială, ci o decizie de management al resurselor și al complexității codului.

Deși ambele paradigme permit implementarea unor mecanisme de control al concurenței (prin semafoare) și strategii de degradare controlată, comportamentul lor sub sarcină critică dictează scenariul optim de utilizare.

Verdictul arhitectural

Pentru sistemele moderne enterprise care integrează LLM-uri, threadurile virtuale reprezintă adesea compromisul pragmatic. Ele acceptă o penalizare controlată în eficiența resurselor hardware în schimbul unei reduceri drastice a complexității software-ului. Cu toate acestea, dacă constrângerile de buget sau volumul de trafic (λ) impun o densitate maximă a cererilor per watt/procesor, paradigma reactivă rămâne bariera tehnologică de neatins pentru modelul thread-per-request.

Sistem de evaluare Am rulat o aplicație cu Quarkus 3.35.2 pe JDK 21 unde un singur serviciu expune în patru moduri diferite același pipeline RAG - o interogare de 20ms în baza de date vectorială, urmată de o așteptare de 20s de la LLM. Toate configurațiile folosesc același WebClient Vert.x și un pool comun de 50.000 de conexiuni, asigurându-ne astfel că poolul upstream nu reprezintă în niciun caz factorul limitator. Ca sursă avem un HttpServer pe threaduri virtuale. Limitele simulate pentru pod sunt -Xmx2g XX:ActiveProcessorCount=4 fără setarea -Xms, ceea ce face ca RSS să reflecte consumul real de memorie. Requesturile au fost generate cu k6 în regim constant-arrival-rate, iar faza de warmup a fost rulată și apoi eliminată din rezultate.

La λ =500 (Tabelul 2), procesarea unui milion de cereri durează 0,556 ore, ceea ce înseamnă că configurația blocantă consumă 28,3 pod-hours per milion de cereri, față de doar 0,556 în cazul celei optimizate — un factor de eficiență de 50,9. Același raport se aplică și în ecuația indicatorului de sustenabilitate software SCI = (E×I + M)/R, deoarece atât componenta energetică, cât și cea a emisiilor încorporate scalează direct proporțional cu numărul de noduri utilizate.

Configurație λ max /
pod
p99 la
capacitate
Amprentă
RAM
(RSS)
Consum
CPU
Thread-uri
OS
Concurență susținută Pod-uri pt.
λ=500
Rata
prezisă
Worker pool (200) 9,96
req/s
20.082 ms 181 MB 3,0% 210 182 51 9,99
req/s
Reactiv (Mutiny) >1.000
req/s
20.126 ms 841 MB 20,0% 29 21.369 1 1,00
req/s
Virtual
Threads
>1.000
req/s
20.168 ms 917 MB 34,0% 38 21.118 1 1,00
req/s

Tabelul 2 - Date experiment

Referințe

  1. J. D. C. Little. "A Proof for the Queuing Formula: L = λW." Operations Research 9(3), 1961, pp. 383-387.

  2. JEP 491: Synchronize Virtual Threads without Pinning. OpenJDK, Java 24. https://openjdk.org/jeps/491

  3. Quarkus. Ghidul Virtual Threads. https://quarkus.io/guides/virtual-threads

  4. G. Tene. How NOT to Measure Latency. Azul Systems. (Coordinated omission; motivul pentru care este necesar un generator de încărcare cu model deschis.)

  5. Netflix. concurrency-limits — limite adaptive de concurență pentru servicii JVM - https://github.com/Netflix/concurrency-limits

  6. Green Software Foundation. Software Carbon Intensity (SCI) Specification, publicată ca ISO/IEC 21031:2024 - https://sci.greensoftware.foundation

  7. P. Lewis et al. "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks." NeurIPS, 2020. arXiv:2005.11401.