A magazintémától a projektgyakorlatig
A bejegyzéshez tartozó szolgáltatási és technikai oldalak
Miért bukik a „High Performance“ gyakran a párhuzamosság miatt REST-nél Delphi-ben
Egy High Performance REST Server Delphi a gyakorlatban ritkán pusztán a kérésenkénti CPU-idő által korlátozott; sokkal gyakrabban az ellenőrizetlen párhuzamosság okozza a problémát: túl sok egyidejű kérés, túl sok egyidejű adatbázis-lekérdezés vagy blokkoló I/O (fájl, hálózat, adatbázis). A jelenség nem úgy jelenik meg, mint „egy kicsit lassabb”, hanem láncreakcióként: több szál, hosszabb várakozási sorok, connection-pool összeomlás, növekvő késleltetések, kliensoldali timeoutek, és végül egy szerver, amely bár „él“, de már nem ad stabil válaszokat.
A ellenszer nem egyetlen trükk, hanem egy tudatos túlterhelés-kezelési viselkedés: amikor a szerver eléri a határait, korán és determinisztikusan vissza kell utasítania (tipikusan HTTP 429 vagy 503), ahelyett hogy a kéréseket egy végtelen várólistára engedné. Erre szolgál ez a forráskódrészlet: egy könnyűsúlyú Concurrency-Gate (semaphore) timeoutokkal, amely beilleszthető meglévő REST-endpointokba – függetlenül attól, hogy Indy-t, WebBroker-t, Horse-t vagy egy saját HTTP-réteget használ.
Architekturidee: Concurrency-Gate vor dem „teuren Teil”
A lényege egyszerű: a költséges rész (adatbázis-hozzáférés, összetett riportok, nagy JSON-válaszok) előtt egy tokent lefoglalunk egy semaphore-ból. Ha nincs szabad token, azonnal kontrollált választ adunk. Fontos: ezt a kaput megbízhatóan fel kell szabadítani (try/finally), és a tényleg költséges kódfutási útra kell beilleszteni — nem csak a request-handler legelején, amikor utána még parser/router/azonosítás következik.
Így a terhelés nem „eltüntetésre“ kerül, hanem csatornázva: a szerver kevesebb kérést válaszol meg egyidejűleg, cserébe stabilabb késleltetésekkel. Egyedi vállalati alkalmazásokban ez többnyire értékesebb, mint szintetikus benchmarkok alkalmankénti csúcsidői.
Source-Schnipsel: Request-Limiter mit Timeout, 429/503 und Telemetrie-Hooks
Az alábbi Delphi-kód egy Concurrency-Gate-et valósít meg TRestRequestGate osztályként. TSemaphore-ra épül (a System.SyncObjs-ból; a semaphore egy számláló a korlátozott egyidejű hozzáférésekhez). A gate-hívás vagy egy „Lease“-objektumot ad vissza (RAII-szerű: felszabadítás a destruktorban), vagy azonnali túlterhelés-választ dönt. Emellett vannak hook-ok naplózáshoz/monitorozáshoz, hogy üzem közben lássa Ön, miért utasítottak el kéréseket.
unit RESTRequestGate;
interface
uses
System.SysUtils,
System.Classes,
System.SyncObjs,
System.Diagnostics;
type
// Minimális kontextus a naplózáshoz/trace-hez; beispielsweise bővíthető felhasználó-/útvonal-információval.
TRESTGateContext = record
RequestId: string;
Route: string;
RemoteIp: string;
end;
TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);
// Hook az üzemeltetési telemetriához (pl. fájl, Syslog, Prometheus-exporter stb.)
TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
Decision: TRESTOverloadDecision;
WaitedMs: Integer;
InFlight: Integer);
// Lease-objektum: a token felszabadítása a destruktorban.
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: nincs várakozás, azonnali 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;
// Először a számlálót csökkentjük, majd a szemafort adjuk vissza.
TInterlocked.Decrement(FInFlightCounter^);
FSemaphore.Release;
end;
{ TRESTRequestGate }
constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
inherited Create;
if AMaxInFlight <= 0 then
raise EArgumentException.Create('AMaxInFlight > 0-nak kell lennie');
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 akkor fordul elő, ha TimeoutMs > 0: célzott várakozás, de korlátozott.
Decision := odRejectedTimeout;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
else
begin
// wrAbandoned/hiba esetén: konzervatívan visszautasítjuk
Decision := odRejectedBusy;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
end;
end;
end.Cél: Terhelés alatt stabilitás a „minden egyszerre” helyett
Mit MaxInFlight segítségével meghatározza, hogy hány Request egyszerre léphet be a „költséges részbe”. Ez szándékosan nem az „CPU-magok száma”, hanem egy üzemeltetési méret. Adatbázis-intenzív végpontoknál gyakran érdemes a MaxInFlight-et a DB-Connection-Poolhoz viszonyítani (például Pool = 20, MaxInFlight = 12 bis 16), hogy ne blokkoljon minden Request egy kapcsolatot, és ne induljanak be további szálak.
Korlátozó feltételek és buktatók
- Try/Finally kötelező: A lease-et garantáltan fel kell szabadítani. Ha az endpointon kivételek fordulnak elő, a kapu „szivárogni” kezd, és a szerver tartósan „busy” állapotban maradhat.
- Timeout ésszerű beállítása:
TimeoutMs=0kemény korlát (azonnali elutasítás). Egy rövid timeout (tipikusan 50 bis 150 ms) kisimítja a csúcsokat anélkül, hogy valódi várólistákat hozna létre. - Kaput ne tévessze alkalmazás korai fázisába: Az autentikáció (például Bearer/JWT) vagy a routing előnyös helyen lehet; a számláló (Semaphore) akkor kapcsoljon be, amikor a ténylegesen költséges szakasz kezdődik. Fordított esetben: ha az auth költséges lesz (pl. külső identity-rendszer ellen), azt is korlátozni kell.
- 429 vs 503: HTTP 429 („Too Many Requests”) jól illik, ha a kliensek célzott retry-t kell, hogy végezzenek. A 503 („Service Unavailable”) akkor megfelelő, ha a szolgáltatás ideiglenesen általánosságban nem képes érdemben fogadni kéréseket. Mindkét esetben egy
Retry-After-Header ajánlott.
Integration in REST-Handler: Indy/WebBroker/Horse pragmatisch
A snippet szándékosan framework-semleges. Csak egy pont kell, ahol a Requestek „végighaladnak”. Tipikus megoldás egy globális singleton vagy egy kapu route-csoportonként (például a „/reports“ kisebb kapu, a „/health“ kapu nélkül). Példa a beépítés mintaként:
- Kontextus kitöltése (RequestId, Route, RemoteIp)
TryAcquirerövid timeouttal- Elutasítás esetén azonnal Response írása (429/503) és befejezés
- A lease a scope-ban él a költséges rész utánig
Horse (Middleware) esetén a kapu közel van a route-csoporthoz. WebBrokerben az adott Action-Handlerben dolgozhat. Indy esetén az a kérdés, hogy szálonként kezel-e kérést; a kapu akkor is működik, amíg a költséges szakaszokat tisztán korlátozza.
High Performance REST Server Delphi: Overload-Antworten, die Clients nicht „vergiften”
Az túlterhelésre adott válaszok többek egyszerű státuszkódoknál. Ha a kliensek 429/503 esetén agresszívan azonnal újrapróbálkoznak, retry-storm alakulhat ki. Heterogén rendszereknél (Mobile Apps, C# Services, legacy kliensek) segít a következetes viselkedés:
- Retry-After: például 1 bis 3 másodperc, végponttól függően. Ez világos ütemadó.
- Rövid body: Egy kis JSON, például
{"error":"server_busy","requestId":"..."}, elegendő. Nagy hibajelentések újra CPU-t és sávszélességet emésztenek fel. - Health-endpoint nem korlátozva: A monitoringnak terhelés alatt is kell információt adnia (szükség esetén „degraded” flag-gel).
Ha előtte reverse proxy-t (például nginx) futtat: hangolja össze ott a timeoutekat és bufferelést. Egy proxy tehermentesíthet (TLS-Termination, Keep-Alive), de a terhelést el is tolhatja (például nagy Request-body-k pufferezése). A gyakorlatban fontos, hogy a limitértékek következetesek legyenek: Proxy-Timeout > App-Timeout, különben a kliensek „Gateway Timeout”-ot látnak, még akkor is, ha az alkalmazás rendesen elutasított volna.
Threading, DB-poolok és Keep-Alive: Hol csúszik el a gyakorlatban
A Gate megoldja a „zu viele gleichzeitig” problémát, de ez nem akadályozza meg automatikusan, hogy egyetlen Request aránytalanul sok erőforrást kössön le. Három tipikus töréspont az Delphi-projektekből pontosan a Threading, az adatbázis és a HTTP-kapcsolatok közötti határfelületeken jelentkezik:
- Egy Request több szűkös erőforrást blokkol: Először egy DB-kapcsolat, aztán egy külső HTTP-call, majd egy fájlhozzáférés. Ha mindez ugyanabban a Request-threadben történik, a blokkolási idő megsokszorozódik. A Gate ugyan korlátozza a párhuzamosságot, de az áteresztőképesség drasztikusan csökken. Itt érdemes az összefüggéseket lazítani (pl. külső hívások aszinkron, előkalkuláció Job-Queue-val).
- BDE-kiváltás natív csatlakozással-Pool-kezelés és tranzakciók: BDE-Ablosung mit nativer Anbindung képes Connection-öket poololni, de egy „hosszú” tranzakció (például ha a JSON-előállítás vagy üzleti ellenőrzések a StartTransaction és a Commit közé esnek) feleslegesen fogja a kapcsolatot. Jó gyakorlat, hogy a tranzakciót a tényleges utasításokra a lehető legszűkebbre szorítsuk, és ahol a szakmai logika engedi, a tranzakción kívül szerializáljunk vagy validáljunk.
- HTTP Keep-Alive mint rejtett memóriafogyasztó: Keep-Alive csökkenti a handshakeket, de sok tétlen kliens esetén túl sok nyitott sockethez vezethet. Különösen Windows- és Linux-szolgáltatások esetén nem „CPU magas”, hanem „Handles/FDs tele” vagy puffer miatti RAM-növekedés látszik. Itt segítenek egyértelmű idle-timeoutok a szerveren és a reverse proxy-n, valamint IP-alapú limit, ha a környezet megengedi.
A következmény: MaxInFlight nem statikus érték. Az Ön leglassabb, legszűkebb erőforrásától függ (DB, külső rendszerek, Storage) és attól, hogy egy Request milyen mértékben „együtt tartja” ezeket az erőforrásokat.
Performance-Hebel neben dem Gate: JSON, DB und I/O nicht vermischen
A Gate stabilizál, de nem helyettesíti a tiszta Endpoint-ökonómiát. Három fékező tényező az Delphi REST-szervereken ismétlődően felbukkan:
- JSON-építés felesleges köztes stringekkel: Gyakran terhelést okoznak a sok ideiglenes Unicode-string. Ahol lehet, streaming-orientáltan építsünk (Writer/Stream) a hatalmas köztes objektumok helyett, különösen listavégpontoknál.
- Adatbázis-hozzáférés „tételenként”: N+1-Queries és soronkénti lekérések a klasszikus probléma. Jobb: célzott JOIN-ok, batch-lekérdezések, szerveroldali aggregáció. Nagyon nagy találatoknál érdemes további lapozást alkalmazni stabil rendezéssel (hogy az oldalak ne „ugráljanak”).
- Blokkoló I/O a Request-threadben: Fájlhozzáféréseket vagy külső HTTP-hívásokat vagy szigorúan korlátozni kell, vagy aszinkron pipeline-ba áthelyezni. Ellenkező esetben drága threade-eket blokkolnak a „várakozás” idejére.
Gyakran ez a töréspont érett, vállalati digitális megoldásoknál: egy végpontot „gyorsan” hozzáadtak és működik, míg a valós terhelés és adatmennyiség meg nem jelenik. Ekkor derül ki, hogy az architekturális határokat tisztán meghúzták-e (adat-hozzáférési réteg, cache-elés, bulk-stratégiák, egyértelmű timeoutok).
Debugging und Betrieb: Was Sie messen sollten
A Hook OnEvent szándékosan egyszerű. A gyakorlatban legalább a következő értékeket kell gyűjteni:
- InFlight (aktuális párhuzamosság a Gate-nél)
- WaitedMs (mennyire engednek „queueing”-et)
- Decision (accepted/busy/timeout)
- Route/RemoteIp (durva okok feltárása, az adatvédelmet figyelmen kívül hagyása nélkül)
Ez jelzést ad arról, hogy a korlátok túl szigorúak-e (túl sok 429) vagy túl enyhék (magas WaitedMs, növekvő késleltetések). És látható, hogy egyes útvonalak dominálnak-e. Windows- és Linux-Services esetén ez a gyakorlatban döntő: telemetria nélkül egy teljesítményprobléma gyorsan találgatássá válik a hálózat, az adatbázis, a proxy és az alkalmazás között.
Szokatlan, de rendkívül hasznos: „WaitedMs” mint korai figyelmeztető jel
Sok csapat csak a válaszidőt és a CPU-t nézi. WaitedMs gyakran jobb mutató, mert megmutatja, hogy a kérések már a tényleges munka előtt várakoznak. Ha a WaitedMs emelkedik, miközben a CPU mérsékelt marad, akkor a szűkös erőforrás gyakran nem a CPU, hanem egy pool (DB-kapcsolatok), egy zárolás az üzleti logikában vagy egy külső downstream szolgáltatás. Ez időt takarít meg a hibaokok feltárásánál, mert célzottabban kereshetünk „Pool/Zárolás/I/O” irányban a „fordító-optimalizálás” helyett.
Variánsok: Pro-Route-Gates, prioritások és „Fast Lane”
Egy mindent lefedő Gate egyszerű, de nem mindig ideális. Értelmes változatok:
- Gate pro Route-Gruppe: „/reports” szigorú, „/api/orders” mérsékelt, „/health” nyitott. Így megakadályozza, hogy a költséges riportkérések kiszorítsák az alapvető folyamatokat.
- Fast Lane für Admin/Monitoring: külön Gate kis párhuzamossággal, hogy az üzemeltetési műveletek terhelés alatt is végrehajthatók legyenek.
- Budget-basierte Limits: Ha a válaszméretek erősen változnak, hasznos lehet kiegészítésként egy bájt-költségvetés (pl. egyszerre legfeljebb X MB generálása). Ez összetettebb, de nagy letöltések esetén reális.
Fontos: a priorizálás gyorsan politikai kérdéssé válik („az én Endpointom fontosabb”). Technikailag stabil marad, ha a prioritások folyamatokhoz vannak kötve (pl. megrendelésfelvétel a riportálás előtt), nem szerepekhez vagy részlegekhez.
Következtetés: Megéri a Gate — és hol bukik meg a megközelítés?
A Concurrency-Gate pragmatikus építőelem egy High Performance REST szerverhez Delphi-ben, mert a túlterhelést kontrollálhatóvá teszi és stabilan tartja rendszereit csúcs terhelés alatt. Különösen érdemes, ha adatbázishoz kötött végpontjai vannak, ha egy Reverse Proxy áll előtte, vagy ha több kliens (Legacy, portálok, Services) hullámszerűen generál terhelést.
A határok egyértelműek: ha a kérések tényleges feldolgozása túl költséges (ineffiziente Queries, große JSON-Objekte, blokkoló külső rendszerek), a Gate csak elfedi a tüneteket. Ilyenkor az adatelérés, a cache-stratégiák, a timeouthoz kapcsolódó beállítások és szükség esetén az aszinkron feldolgozás (Queue/Job-System) javítása szükséges. Üzemeltetési biztonsági övként a Gate azonban gyakran a különbség az „ideiglenesen használható” és a „teljesen használhatatlan” között.
Ha a túlterhelési viselkedést egy meglévő Delphi REST-API und REST-Server környezetbe szeretné bevezetni, vagy ha a limiteket adatbázis- és proxy-timeoutokkal szeretné tisztán kiegyensúlyozni: beszélje meg a projektet vagy modernizációs tervet Net-Base-vel.
A szakmai környezetben a Thread-Pool Delphi és a Http 429 Too Many Requests is fontos szerepet játszik, ha az integrációknak, az adatfolyamoknak és a további fejlesztésnek szorosan együtt kell működnie.
Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.
Következő lépés
Ha egy témából valós projekt lesz, az architektúrát, a meglévő rendszert és az üzemeltetést korai fázisban együtt kell vizsgálni.
Nemcsak egyedi kérdésekben támogatunk, hanem akkor is, amikor forráskódrészletekből, örökölt rendszerekkel kapcsolatos témákból vagy portálötletekből robusztus vállalati projektet kell kialakítani.
- A jelenlegi állapotot, a célállapotot és a műszaki kockázatokat együttesen értékeljük.
- REST, az adathozzáférést, a portálokat és a bevezetést nem halasztjuk későbbi fázisokra.
- Ön korán látja, melyik út gazdaságilag és üzemeltetési szempontból tartható.