Od témy magazínu k projektovej praxi
Súvisiace stránky služieb a technológií k príspevku
Prečo „High Performance“ pri REST v Delphi často zlyháva na súbežnosti
V praxi nie je High Performance REST Server Delphi zriedka limitovaný čistým CPU-časom na požiadavku, ale nekontrolovanou súbežnosťou: príliš veľa súbežných požiadaviek, príliš veľa súbežných databázových dotazov alebo blokujúce I/O (súbor, sieť, databáza). Výsledok potom nepôsobí ako „trochu pomalšie“, ale ako reťazová reakcia: viac vlákien, viac čakacích front, kolaps poolu pripojení, rastúce latencie, timeouty na strane klienta a nakoniec server, ktorý síce ešte „žije“, ale už neposkytuje stabilné odpovede.
Protiopatrenie nie je jeden trik, ale vedomé Overload-Verhalten: keď sa server priblíži k svojim hraniciam, musí včas a deterministicky odmietať (typicky HTTP 429 alebo 503), namiesto toho, aby nechal požiadavky bežať do nekonečnej čakacej fronty. Práve na to je určený tento source-schnipsel: ľahká brána súbežnosti (semafor) s timeoutmi, ktorú je možné integrovať do existujúcich REST-endpointov – nezávisle od toho, či používate Indy, WebBroker, Horse alebo vlastnú HTTP vrstvu.
Architektúrna myšlienka: brána súbežnosti pred „nákladnou časťou“
Základná myšlienka je jednoduchá: pred nákladnou časťou (prístup k databáze, komplexné reporty, veľké JSON odpovede) sa rezervuje token zo semafóru. Ak nie je voľný token, vráti sa okamžitá kontrolovaná odpoveď. Dôležité je: táto brána sa musí spoľahlivo uvoľniť (try/finally), a musí byť vložená do kódu, ktorý je skutočne nákladný – nie len úplne na začiatku spracovača požiadaviek, keď potom nasleduje parser/router/autentifikácia.
Tým sa záťaž neodstráni „optimalizáciou“, ale zkanalizuje: server odpovedá na menej požiadaviek súbežne, no s stabilnejšími latenciami. V individuálnych podnikových aplikáciách je to väčšinou hodnotnejšie ako občasné rekordy v syntetických benchmarkoch.
Source-Schnipsel: limitér požiadaviek s timeoutom, 429/503 a telemetrickými hookmi
Nasledujúci Delphi-kód implementuje bránu súbežnosti ako triedu TRestRequestGate. Zakladá sa na TSemaphore (z System.SyncObjs; semafor je čítač pre obmedzené súbežné prístupy). Volanie brány vráti buď „Lease“-objekt (RAII-ähnlich: uvoľnenie v destruktore) alebo sa rozhodne pre okamžitú odpoveď pri preťažení. Navyše sú k dispozícii hooky pre logovanie/monitorovanie, aby ste v prevádzke videli, prečo boli požiadavky odmietnuté.
unit RESTRequestGate;
interface
uses
System.SysUtils,
System.Classes,
System.SyncObjs,
System.Diagnostics;
type
// Minimálny kontext pre logovanie/traceovanie; môže byť napr. rozšírený o používateľa a trasu.
TRESTGateContext = record
RequestId: string;
Route: string;
RemoteIp: string;
end;
TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);
// Hook pre prevádzkovú telemetriu (napr. do súboru, Syslog, Prometheus exportéra, atď.)
TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
Decision: TRESTOverloadDecision;
WaitedMs: Integer;
InFlight: Integer);
// Lease-objekt: uvoľnenie tokenu v destruktore.
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: žiadne čakanie, okamžite 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;
// Najprv znížiť čítač, potom uvoľniť semafor.
TInterlocked.Decrement(FInFlightCounter^);
FSemaphore.Release;
end;
{ TRESTRequestGate }
constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
inherited Create;
if AMaxInFlight <= 0 then
raise EArgumentException.Create('AMaxInFlight musí byť > 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 pri TimeoutMs > 0: cielene čakať, ale s obmedzením.
Decision := odRejectedTimeout;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
else
begin
// wrAbandoned/chybové prípady: konzervatívne odmietnuť
Decision := odRejectedBusy;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
end;
end;
end.Účel: stabilita pri zaťažení namiesto „všetko naraz“
Pomocou MaxInFlight definujete, koľko požiadaviek môže súčasne vstúpiť do „nákladnej časti“. To zámerne nie je „počet jadier CPU“, ale prevádzková veličina. Pri endpointoch s vysokou záťažou databázy má často zmysel nastaviť MaxInFlight v pomere k DB-connection-poolu (napríklad Pool = 20, MaxInFlight = 12 až 16), aby každá požiadavka nezablokovala pripojenie a následne nezatáhla ďalšie vlákna.
Okrajové podmienky a úskalia
- Try/Finally ist Pflicht: Der Lease muss garantiert freigegeben werden. Wenn Sie Exceptions im Endpoint haben, wird sonst das Gate „undicht“ und der Server bleibt dauerhaft auf „busy“.
- Timeout sinnvoll wählen:
TimeoutMs=0ist ein hartes Limit (sofort abweisen). Ein kurzes Timeout (typisch 50 bis 150 ms) glättet Peaks, ohne echte Warteschlangen aufzubauen. - Gate nicht zu früh: Authentifizierung (zum Beispiel Bearer/JWT) oder Routing kann günstig sein; die Semaphore sollte vor dem wirklich teuren Abschnitt greifen. Umgekehrt: Wenn Auth teuer wird (z.B. gegen ein externes Identity-System), muss auch das begrenzt werden.
- 429 vs 503: HTTP 429 („Too Many Requests“) passt gut, wenn Clients gezielt retryen sollen. 503 („Service Unavailable“) passt, wenn der Dienst temporär generell nicht in der Lage ist, Anfragen sinnvoll anzunehmen. In beiden Fällen ist ein
Retry-After-Header empfehlenswert.
Integration in REST-Handler: Indy/WebBroker/Horse pragmatisch
Der Snippet ist absichtlich framework-neutral. Sie brauchen nur einen Ort, an dem Requests „durchlaufen“. Typisch ist ein globales Singleton oder ein Gate pro Route-Gruppe (zum Beispiel „/reports“ kleiner, „/health“ ohne Gate). Beispielhaft die Einbindung als Muster:
- Kontext füllen (RequestId, Route, RemoteIp)
TryAcquiremit kurzem Timeout- Bei Ablehnung sofort Response schreiben (429/503) und beenden
- Lease lebt im Scope bis nach dem teuren Teil
In Horse (Middleware) liegt das Gate nahe an einer Route-Gruppe. In WebBroker können Sie im jeweiligen Action-Handler arbeiten. Bei Indy hängt es davon ab, ob Sie pro Request einen Thread haben; das Gate wirkt trotzdem, solange die teuren Abschnitte sauber begrenzt werden.
High Performance REST Server Delphi: Overload-Antworten, die Clients nicht „vergiften“
Überlast-Antworten sind mehr als Statuscodes. Wenn Clients bei 429/503 aggressiv sofort erneut senden, haben Sie einen Retry-Sturm. In heterogenen Systemlandschaften (Mobile Apps, C# Services, Legacy-Clients) hilft ein konsistentes Verhalten:
- Retry-After: zum Beispiel 1 bis 3 Sekunden, je nach Endpoint. Das ist ein klarer Taktgeber.
- Kurzer Body: Ein kleines JSON wie
{"error":"server_busy","requestId":"..."}reicht. Große Error-Objekte kosten wieder CPU und Bandbreite. - Health-Endpoint ungedrosselt: Monitoring soll auch bei Last noch Aussagen liefern (ggf. mit „degraded“-Flag).
Wenn Sie einen Reverse Proxy wie nginx davor betreiben: Timeouts und Buffering dort abstimmen. Ein Proxy kann entlasten (TLS-Termination, Keep-Alive), aber auch Last verschieben (zum Beispiel große Request-Bodies puffern). Im Betrieb zählt, dass die Limits konsistent sind: Proxy-Timeout > App-Timeout, sonst sehen Clients „Gateway Timeout“, obwohl die App sauber abgewiesen hätte.
Vlákna, DB-pooly a Keep-Alive: Kde to v praxi zlyháva
Das Gate rieši problém „príliš veľa súčasne“, ale automaticky nezabrání tomu, aby jedna požiadavka viazala nadmerné množstvo zdrojov. Tri typické body zlomu z Delphi-projektov vznikajú presne na rozhraní medzi Threading, databázou a HTTP-pripojeniami:
- Jedna požiadavka blokuje niekoľko obmedzených zdrojov: Najskôr DB-pripojenie, potom externé HTTP-volanie, potom prístup k súboru. Ak sa všetko deje v tom istom vlákne požiadavky, čas blokovania sa násobí. Gate síce obmedzí paralelitu, ale priepustnosť prudko klesne. Oplatí sa preto závislosti oddeliť (napr. externé volania asynchrónne, predvýpočet cez frontu úloh).
- BDE-Ablosung mit nativer Anbindung-Pooling und Transaktionen: BDE-Ablosung mit nativer Anbindung môže spravovať pool pripojení, ale „dlhá“ transakcia (napr. preto, že tvorba JSON alebo business-checky prebiehajú medzi StartTransaction a Commit) drží spojenie zbytočne. Dobrá prax je obmedziť transakciu čo najužšie na skutočné SQL-príkazy a serializáciu alebo validáciu robiť mimo transakcie, ak to aplikačná logika dovolí.
- HTTP Keep-Alive ako skrytý žrút pamäte: Keep-Alive znižuje počet handshake-ov, ale pri veľkom počte neaktívnych klientov môže viesť k príliš mnohým otvoreným socketom. Najmä pri Windows- und Linux-Services potom nevidíte „CPU hore“, ale „Handles/FDs plné“ alebo rast RAM kvôli bufferom. Pomáhajú jasné idle-timeouty na serveri aj na reverznom proxy a limit na klientsku IP, ak to prostredie umožňuje.
Konzekvencia: MaxInFlight nie je statická hodnota. Závisí od vašej najpomalšej, najobmedzenejšej ressourcy (DB, externé systémy, storage) a od toho, ako dobre požiadavka tieto zdroje „držia“.
Výkonnostné páky vedľa Gate: JSON, DB a I/O nemiešať
Gate stabilizuje, ale nenahrádza čistú ekonómiu endpointov. Tri brzdy v Delphi REST-serveroch sa opakovane objavujú:
- Generovanie JSON s zbytočnými medzireťazcami: Často vzniká zaťaženie kvôli mnohým dočasným Unicode reťazcom. Kde je to možné, konštruujte orientovane na stream (Writer/Stream) namiesto vytvárania veľkých medziobjektov, obzvlášť pri endpointoch vracajúcich zoznamy.
- Prístup do databázy „na položku“: N+1 dotazy a lookupy per-row sú klasika. Lepšie je cielené JOINovanie, dávkové dotazy (batch queries) alebo server-side agregácia. Pri veľmi veľkých výsledkoch sa oplatí aj stránkovanie (pagination) so stabilným triedením, aby sa strany „nepreskakovali“.
- Blokujúce I/O vo vlákne požiadavky: Prístup k súborom alebo externé HTTP-volania by mali byť buď prísne obmedzené, alebo presunuté do asynchrónnej pipeline. Inak blokujete drahé vlákna len na „čakanie“.
Pre vyrastené digitálne podnikové riešenia je to často kľúčové: endpoint bol „na rýchlo“ doplnený a funguje, až kým nepríde reálne zaťaženie a objemy dát. Potom sa ukáže, či boli hranice architektúry jasne vyznačené (vrstva prístupu k dátam, caching, bulk-stratégie, jasné time-outy).
Debugging und Betrieb: Was Sie messen sollten
Der Hook OnEvent ist bewusst simpel. In der Praxis sollten Sie mindestens folgende Werte erfassen:
- InFlight (aktuálna paralelita na Gate)
- WaitedMs (koľko „queueingu“ dopúšťate)
- Decision (accepted/busy/timeout)
- Route/RemoteIp (hrubá analýza príčin, bez ignorovania ochrany osobných údajov)
Tým získate signál, či sú limity príliš prísne (príliš veľa 429) alebo príliš voľné (vysoké WaitedMs, rastúce latencie). A vidíte, či dominujú jednotlivé trasy. Pre Windows- a Linux-Services je to v bežnej prevádzke rozhodujúce: Bez telemetrie sa problém s výkonom rýchlo zmení na hádanku medzi sieťou, databázou, proxy a aplikáciou.
Neobvyklé, ale mimoriadne užitočné: „WaitedMs“ ako skorý varovný indikátor
Mnohé tímy sledujú iba dobu odozvy a CPU. WaitedMs je často spoľahlivejší indikátor, pretože ukazuje, že requesty už čakajú pred samotnou prácou. Ak WaitedMs rastie, zatiaľ čo CPU zostáva mierne, úzkym miestom často nie je CPU, ale pool (DB-pripojenia), lock v business-logike alebo externý downstream-service. To šetrí čas pri analýze príčin, pretože cielenejšie hľadáte smerom „Pool/Lock/I/O“ namiesto „optimalizácie kompilátora“.
Varianty: brány na úrovni trasy, priority a „Fast Lane“
Brána pre všetko je jednoduchá, ale nie vždy ideálna. Rozumné varianty:
- Brána pre skupinu trás: „/reports“ prísna, „/api/orders“ mierna, „/health“ otvorená. Tým zabránite, aby nákladné reporty vytláčali jadrové procesy.
- Fast Lane pre administráciu/monitoring: samostatná brána s nízkou paralelitou, aby prevádzkové zásahy boli možné aj pri zaťažení.
- Limit založený na rozpočte: Ak sa veľkosti odpovedí výrazne líšia, môže pomôcť aj bajtový rozpočet (napr. maximálne X MB súčasne pri generovaní). Je to zložitejšie, ale pri veľkých stiahnutiach realistické.
Dôležité: Priorizácia rýchlo nadobudne politický rozmer („môj Endpoint je dôležitejší“). Technicky stabilné to zostane, ak sú priority viazané na procesy (napr. zadávanie objednávok pred reportovaním), nie na role alebo oddelenia.
Záver: Má zmysel brána – a kde tento prístup zlyháva?
Ein Concurrency-Gate je pragmatický stavebný prvok pre vysokovýkonný REST Server v Delphi, pretože robí preťaženie kontrolovateľným a udržiava vaše systémy stabilné pri špičkovej záťaži. Obzvlášť sa oplatí, ak máte endpointy viazané na databázu, pred serverom stojí reverse proxy alebo ak viacerí klienti (Legacy, portály, služby) generujú zaťaženie v vlnách.
Limitácie sú jasné: Ak je samotná práca na request príliš nákladná (neefektívne dotazy, veľké JSON-objekty, blokujúce externé systémy), brána iba maskuje symptómy. Potom treba upraviť prístup k dátam, cachingové stratégie, timeouts a prípadne asynchrónne spracovanie (Queue/Job-System). Ako bezpečnostný pás v prevádzke je brána však často rozdiel medzi „krátko zdržaním“ a „úplnou nepoužiteľnosťou“.
Ak chcete zaviesť správanie pri preťažení do existujúcej Delphi REST-API und REST-Server alebo ak chcete limity vyvážiť s databázovými a proxy-timeoutmi: prediskutujte projekt alebo modernizačný zámer s Net-Base.
V odbornom kontexte zohrávajú dôležitú úlohu aj thread-pool Delphi a Http 429 Too Many Requests, keď musia integrácie, dátové toky a ďalší vývoj hladko spolupracovať.
Ďalší krok
Keď sa téma stane reálnym projektom, architektúru, existujúci stav a prevádzku treba včas posudzovať spoločne.
Podporujeme nielen pri jednotlivých otázkach, ale aj vtedy, keď sa z fragmentov zdrojového kódu, tém súvisiacich s legacy systémami alebo nápadov na portál má stať robustný podnikový projekt.
- Stav, cieľový obraz a technické riziká sa hodnotia spoločne.
- REST, prístup k dátam, portály a Rollout nebudú odložené na neskôr.
- Včas zistíte, ktorá cesta je ekonomicky a prevádzkovo životaschopná.