Od teme magazina do projektne prakse
Povezane stranice usluga i tehnologije za članak
Zašto „High Performance“ kod REST u Delphi često ne uspijeva zbog paralelizma
U praksi High Performance REST server Delphi rijetko je ograničen isključivo CPU‑vremenom po zahtjevu, već nekontrolisanom paralelnošću: previše istovremenih zahtjeva, previše istovremenih upita prema bazi podataka ili blokirajući I/O (datoteka, mreža, baza podataka). Posljedica ne izgleda kao „malo sporije“, već kao lančana reakcija: više niti, duži redovi čekanja, kolaps Connection‑Pool‑a, rastuće latencije, Timeouts na strani klijenta i na kraju server koji iako još „živi“, ne daje više stabilne odgovore.
Protivmjera nije pojedinačan trik, već svjesno Overload‑Verhalten: kada server dosegne svoje granice, mora rano i deterministički odbijati zahtjeve (tipično HTTP 429 ili 503), umjesto da pusti zahtjeve u beskonačni red čekanja. Upravo za to je namijenjen ovaj izvorni isječak: lagani Concurrency‑Gate (Semaphore) plus Timeouts, koji se može integrirati u postojeće REST endpoint‑e – bez obzira koristite li Indy, WebBroker, Horse ili vlastiti HTTP‑sloj.
Arhitektonska ideja: Concurrency‑Gate ispred „zahtjevnijeg dijela“
Osnovna ideja je jednostavna: prije zahtjevnog dijela (pristup bazi podataka, složeni izvještaji, velike JSON‑odgovore) rezervira se token iz Semaphore. Ako nema slobodnog tokena, odmah se daje kontrolisani odgovor. Važno je: ovo Gate mora biti pouzdano oslobođeno (try/finally), i mora biti u putanji koda koja je zaista zahtjevna – ne samo na samom početku request‑handlera, kada nakon toga ionako slijede parser/router/autentifikacija.
Na taj način opterećenje se ne „optimizira van“, nego kanalizira: server odgovara manje Requests istovremeno, ali s stabilnijim latencijama. U individualnim poslovnim aplikacijama to je obično vrijednije od sporadičnih najboljih rezultata u sintetičkim Benchmarks.
Izvorni isječak: Request‑Limiter s Timeout‑om, 429/503 i Telemetrijskim Hook‑ovima
Sljedeći Delphi‑kod implementira Concurrency‑Gate kao klasu TRestRequestGate. Temelji se na TSemaphore (iz System.SyncObjs; Semaphore je brojač za ograničene istovremene pristupe). Poziv Gate‑a vraća ili „Lease“‑objekt (slično RAII: oslobađanje u Destructor) ili odluči za trenutni odgovor o preopterećenju. Dodatno postoje hook‑ovi za logiranje/monitoring, kako biste u radu mogli vidjeti, zašto su zahtjevi bili odbijeni.
unit RESTRequestGate;
interface
uses
System.SysUtils,
System.Classes,
System.SyncObjs,
System.Diagnostics;
type
// Minimalni kontekst za logiranje/trace; može npr. biti proširen za korisnika/rutu.
TRESTGateContext = record
RequestId: string;
Route: string;
RemoteIp: string;
end;
TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);
// Hook za operativnu telemetriju (npr. u fajl, Syslog, Prometheus-Exporter, itd.)
TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
Decision: TRESTOverloadDecision;
WaitedMs: Integer;
InFlight: Integer);
// Lease-objekat: oslobađanje tokena u destruktoru.
TRESTGateLease = class
private
FSemaphore: TSemaphore;
FInFlightCounter: PInteger;
FReleased: Boolean;
public
constructor Create(ASem: TSemaphore; ACounter: PInteger);
destructor Destroy; override;
procedure Release;
end;
TRESTRequestGate = class
private
FSem: TSemaphore;
FMaxInFlight: Integer;
FInFlight: Integer;
FOnEvent: TRESTGateEvent;
public
constructor Create(AMaxInFlight: Integer);
destructor Destroy; override;
// TimeoutMs = 0: nema čekanja, odmah 429/503
function TryAcquire(const Ctx: TRESTGateContext; TimeoutMs: Cardinal;
out Lease: TRESTGateLease;
out WaitedMs: Integer;
out Decision: TRESTOverloadDecision): Boolean;
property OnEvent: TRESTGateEvent read FOnEvent write FOnEvent;
property MaxInFlight: Integer read FMaxInFlight;
function InFlight: Integer;
end;
implementation
uses
System.Math;
{ TRESTGateLease }
constructor TRESTGateLease.Create(ASem: TSemaphore; ACounter: PInteger);
begin
inherited Create;
FSemaphore := ASem;
FInFlightCounter := ACounter;
FReleased := False;
end;
destructor TRESTGateLease.Destroy;
begin
Release;
inherited;
end;
procedure TRESTGateLease.Release;
begin
if FReleased then
Exit;
FReleased := True;
// Prvo smanji brojač, pa zatim oslobodi semafor.
TInterlocked.Decrement(FInFlightCounter^);
FSemaphore.Release;
end;
{ TRESTRequestGate }
constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
inherited Create;
if AMaxInFlight <= 0 then
raise EArgumentException.Create('AMaxInFlight mora biti > 0');
FMaxInFlight := AMaxInFlight;
FInFlight := 0;
// InitialCount = MaxCount = AMaxInFlight
FSem := TSemaphore.Create(nil, AMaxInFlight, AMaxInFlight, '');
end;
destructor TRESTRequestGate.Destroy;
begin
FSem.Free;
inherited;
end;
function TRESTRequestGate.InFlight: Integer;
begin
Result := TInterlocked.CompareExchange(FInFlight, 0, 0);
end;
function TRESTRequestGate.TryAcquire(const Ctx: TRESTGateContext; TimeoutMs: Cardinal;
out Lease: TRESTGateLease; out WaitedMs: Integer; out Decision: TRESTOverloadDecision): Boolean;
var
Sw: TStopwatch;
WaitRes: TWaitResult;
CurrentInFlight: Integer;
begin
Lease := nil;
WaitedMs := 0;
Decision := odRejectedBusy;
Sw := TStopwatch.StartNew;
if TimeoutMs = 0 then
WaitRes := FSem.WaitFor(0)
else
WaitRes := FSem.WaitFor(TimeoutMs);
WaitedMs := Integer(Min(Sw.ElapsedMilliseconds, High(Integer)));
case WaitRes of
wrSignaled:
begin
CurrentInFlight := TInterlocked.Increment(FInFlight);
Lease := TRESTGateLease.Create(FSem, @FInFlight);
Decision := odAccepted;
Result := True;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, CurrentInFlight);
end;
wrTimeout:
begin
// wrTimeout kod TimeoutMs > 0: ciljano čekanje, ali ograničeno.
Decision := odRejectedTimeout;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
else
begin
// wrAbandoned/greške: konzervativno odbiti
Decision := odRejectedBusy;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
end;
end;
end.Svrha: Stabilnost pod opterećenjem umjesto „sve odjednom“
Kroz MaxInFlight određujete koliko zahtjeva istovremeno smije ući u „skupi dio“. To izričito nije „broj CPU-jezgara“, već operativna veličina. Kod endpointa s velikim opterećenjem baze podataka često je smisleno postaviti MaxInFlight u odnosu na DB-Connection-Pool (na primjer Pool = 20, MaxInFlight = 12 do 16), kako svaki zahtjev ne bi blokirao vezu i potom povukao dodatne niti.
Uvjeti i zamke
- Try/Finally je obavezno: Lease mora biti zajamčeno oslobođen. Ako imate iznimke u endpointu, inače će Gate postati „propustan“ i server će ostati trajno u „busy“ stanju.
- Timeout promišljeno odabrati:
TimeoutMs=0je tvrda granica (odmah odbaciti). Kratak timeout (tipično 50 do 150 ms) ublažava pikove, bez izgradnje stvarnih redova. - Gate ne postavljati prerano: Autentifikacija (na primjer Bearer/JWT) ili rutiranje može biti korisno; semafor bi trebao zahvatiti prije stvarno skupog dijela. Obrnuto: ako autentifikacija postane skupa (npr. prema vanjskom sustavu identiteta), i ona mora biti ograničena.
- 429 vs 503: HTTP 429 („Too Many Requests“) odgovara kada klijenti trebaju ciljano ponavljati zahtjeve. 503 („Service Unavailable“) odgovara kada usluga privremeno općenito nije u stanju primati zahtjeve. U oba slučaja preporučljiv je
Retry-After-header.
Integracija u REST-Handler: Indy/WebBroker/Horse pragmatično
Ovaj snippet je namjerno framework-neutralan. Trebate samo jedno mjesto kroz koje zahtjevi „prolaze“. Tipično je to globalni singleton ili gate po grupi ruta (na primjer „/reports“ manji, „/health“ bez gate). Kao primjer, ugradnja kao obrazac:
- Popuniti kontekst (RequestId, Route, RemoteIp)
TryAcquires kratkim timeoutom- Pri odbijanju odmah vratiti odgovor (429/503) i prekinuti
- Lease ostaje u opsegu dok se ne završi skupi dio
U Horse (Middleware) Gate je blizu grupe ruta. U WebBrokeru možete raditi u odgovarajućem Action-Handleru. Kod Indyja ovisi o tome imate li po zahtjevu nit; Gate i dalje djeluje, sve dok su skupi odsjeci jasno ograničeni.
High Performance REST Server Delphi: Overload-Antworten, die Clients nicht „vergiften“
Odgovori pri preopterećenju su više od samih statuskodova. Ako klijenti pri 429/503 agresivno odmah ponovo šalju, dobit ćete retry-storm. U heterogenim sustavnim krajolicima (mobilne aplikacije, C# Services, legacy-klijenti) pomaže konzistentno ponašanje:
- Retry-After: na primjer 1 do 3 sekunde, ovisno o endpointu. To je jasan takt.
- Kratko tijelo: Malo JSON-a kao
{"error":"server_busy","requestId":"..."}je dovoljno. Veliki error-objekti ponovno troše CPU i propusnost. - Health-Endpoint bez ograničenja: Monitoring treba i pod opterećenjem davati informacije (eventualno s „degraded“-flagom).
Ako ispred imate reverse proxy poput nginx: uskladite timeout-e i buffering tamo. Proxy može olakšati (TLS-Termination, Keep-Alive), ali i pomaknuti opterećenje (na primjer buferirati velike request-bodyje). U radu je važno da su limiti konzistentni: Proxy-Timeout > App-Timeout, inače klijenti vide „Gateway Timeout“, iako je aplikacija uredno odbila.
Threading, DB-Pools i Keep-Alive: Gdje u praksi dolazi do zastoja
Das Gate löst das „zu viele gleichzeitig“-Problem, aber es verhindert nicht automatisch, dass ein einzelner Request übermäßig viele Ressourcen bindet. Drei typische Kipp-Punkte aus Delphi-Projekten entstehen genau an den Schnittstellen zwischen Threading, Datenbank und HTTP-Verbindungen:
- Jedan Request blokira više ograničenih resursa: Prvo eine DB-Verbindung, dann ein externer HTTP-Call, dann ein Dateizugriff. Wenn das alles im selben Request-Thread passiert, multipliziert sich die Blockadezeit. Das Gate begrenzt dann zwar die Parallelität, aber die Durchsatzleistung sinkt drastisch. Hier lohnt es sich, die Abhängigkeiten zu entkoppeln (z.B. externe Calls asynchron, Vorberechnung per Job-Queue).
- BDE-zamjena s nativnom vezom-Pooliranje und Transaktionen: BDE-Ablosung mit nativer Anbindung kann Connections poolen, aber eine „lange“ Transaktion (z.B. weil JSON-Erstellung oder Business-Checks zwischen StartTransaction und Commit liegen) hält die Verbindung unnötig. Eine saubere Praxis ist, die Transaktion so eng wie möglich um die eigentlichen Statements zu legen und außerhalb der Transaktion zu serialisieren oder zu validieren, wenn es fachlich geht.
- HTTP Keep-Alive als versteckter Speicherfresser: Keep-Alive reduziert Handshakes, kann aber bei vielen Leerlauf-Clients zu vielen offenen Sockets führen. Gerade bei Windows- und Linux-Services sieht man dann nicht „CPU hoch“, sondern „Handles/FDs voll“ oder RAM durch Puffer. Hier helfen klare Idle-Timeouts im Server und am Reverse Proxy sowie ein Limit pro Client-IP, wenn es die Umgebung erlaubt.
Die Konsequenz: MaxInFlight ist kein statischer Wert. Er hängt von Ihrer langsamsten, knappsten Ressource ab (DB, externe Systeme, Storage) und davon, wie gut ein Request diese Ressourcen „zusammenhält“.
Performance-Hebel neben dem Gate: JSON, DB und I/O nicht vermischen
Das Gate stabilisiert, aber es ersetzt keine saubere Endpoint-Ökonomie. Drei Bremsen in Delphi REST-Servern tauchen wiederholt auf:
- JSON-Building mit unnötigen Zwischenstrings: Häufig entsteht Last durch viele temporäre Unicode-Strings. Wo möglich, streaming-orientiert bauen (Writer/Stream) statt riesige Zwischenobjekte, besonders bei Listen-Endpunkten.
- Datenbankzugriff „pro Item“: N+1-Queries und per-Row Lookups sind der Klassiker. Besser: gezielte Joins, Batch-Queries, serverseitige Aggregation. Bei sehr großen Ergebnissen lohnt sich zusätzlich Pagination mit stabiler Sortierung (damit Seiten nicht „springen“).
- Blockierende I/O im Request-Thread: Dateizugriffe oder externe HTTP-Calls sollten entweder strikt begrenzt oder in eine asynchrone Pipeline verlagert werden. Sonst blockieren Sie teure Threads für „Warten“.
Für gewachsene digitale Unternehmenslösungen ist das oft der Knackpunkt: Ein Endpoint wurde „mal schnell“ ergänzt und funktioniert, bis reale Last und Datenvolumina kommen. Dann zeigt sich, ob Architekturgrenzen sauber gezogen wurden (Datenzugriffsschicht, Caching, Bulk-Strategien, klare Timeouts).
Debugging und Betrieb: Was Sie messen sollten
Der Hook OnEvent ist bewusst simpel. In der Praxis sollten Sie mindestens folgende Werte erfassen:
- InFlight (aktuelle Parallelität am Gate)
- WaitedMs (wie viel „Queueing“ Sie zulassen)
- Decision (accepted/busy/timeout)
- Route/RemoteIp (gruba analiza uzroka, bez ignoriranja zaštite podataka)
To vam daje signal da li su limiti previše strogi (previše 429) ili preslabi (visoki WaitedMs, rastuće latencije). I vidite da li pojedinačne rute dominiraju. Za Windows- i Linux-Services je to u praksi presudno: bez telemetrije problem sa performansama brzo postaje pogađanje između mreže, baze podataka, proxyja i aplikacije.
Neobično, ali izuzetno korisno: „WaitedMs“ kao rani indikator
Mnogi timovi gledaju samo na vrijeme odziva i CPU. WaitedMs je često bolji indikator, jer pokazuje da zahtjevi već čekaju prije same obrade. Ako WaitedMs raste dok CPU ostaje umjerena, usko grlo često nije CPU, već pool (DB-veze), lock u poslovnoj logici ili eksterni downstream-servis. To štedi vrijeme pri analizi uzroka, jer tražite ciljano prema „Pool/Lock/I/O“ umjesto prema „optimizaciji kompajlera“.
Varijante: Gate po ruti, prioriteti i „Fast Lane“
Jedno gate za sve je jednostavno, ali ne uvijek idealno. Korisne varijante:
- Gate po grupi ruta: „/reports“ strogo, „/api/orders“ umjereno, „/health“ otvoreno. Tako sprječavate da skupi zahtjevi za izvještajima potisnu ključne procese.
- Fast Lane za Admin/Monitoring: Odvojeno gate sa malom paralelnošću, kako bi operativne radnje bile moguće i pod opterećenjem.
- Na osnovu budžeta postavljena ograničenja: Ako se veličine odgovora jako razlikuju, dodatni byte-budžet može pomoći (npr. maksimalno X MB istovremeno pri generiranju). To je složenije, ali realno za velike preuzimanja.
Važno: prioritetizacija brzo postane politička („moj Endpoint je važniji“). Tehnički stabilno ostaje ako su prioriteti vezani uz procese (npr. unos narudžbi prije izvještavanja), a ne uz uloge ili odjele.
Zaključak: Isplati li se gate – i gdje pristup zakaže?
Concurrency-Gate je pragmatičan element za High Performance REST server u Delphi, jer čini preopterećenje kontroliranim i održava vaše sustave stabilnim pri vršnom opterećenju. Posebno se isplati ako imate endpoint-e vezane za bazu podataka, ako im prethodi reverse proxy ili ako više klijenata (Legacy, portali, servisi) u valovima generira opterećenje.
Granice su jasne: ako je stvarni rad po zahtjevu preskup (neefikasni upiti, veliki JSON-objekti, blokirajući vanjski sustavi), gate samo maskira simptome. Tada se moraju poboljšati pristup podacima, strategije keširanja, timeouti i eventualno asinkrona obrada (Queue/Job-System). Kao sigurnosni pojas u radu, gate je često razlika između „kratkoročno tromo“ i „potpuno neupotrebljivo“.
Ako želite implementirati ponašanje pri preopterećenju u postojeću Delphi REST-API und REST-Server ili precizno izbalansirati limite s timeoutima baze podataka i proxyja: razgovarajte o projektu ili modernizacijskom poduhvatu s Net-Base.
U stručnom okruženju važnu ulogu igraju i Thread-Pool Delphi i Http 429 Too Many Requests, kada integracije, tokovi podataka i dalji razvoj moraju uredno surađivati.
Razgovarajte o projektu ili modernizacijskom poduhvatu s Net-Base.
Sljedeći korak
Ako se tema pretvori u stvarni projekat, arhitekturu, postojeći sistem i operacije trebalo bi rano zajednički razmotriti.
Pružamo podršku ne samo pri pojedinačnim pitanjima, već i kada iz fragmenata izvornog koda, naslijeđenih sistema ili ideja za portal treba nastati robustan poslovni projekat.
- Postojeće stanje, ciljno stanje i tehnički rizici procjenjuju se zajedno.
- REST, pristup podacima, portali i Rollout neće se odgađati za kasnije faze.
- Pravovremeno prepoznajete koji pristup je ekonomski i operativno održiv.