Od tématu magazínu k projektové praxi
Vhodné stránky služeb a technické stránky k příspěvku
Proč „High Performance“ u REST v Delphi často naráží na paralelitu
V praxi nebývá High Performance REST Server Delphi omezen čistě CPU časem na požadavek, ale nekontrolovanou paralelností: příliš mnoho současných požadavků, příliš mnoho současných dotazů do databáze nebo blokující I/O (soubor, síť, databáze). Výsledek pak nevypadá jako „trochu pomalejší“, ale jako řetězová reakce: více vláken, více front, kolaps connection poolu, rostoucí latence, time-outy na straně klienta a nakonec server, který sice ještě „žije“, ale už nedoručuje stabilní odpovědi.
Protiopatření není jediný trik, ale cílené chování při přetížení: když server dosáhne svých hranic, musí odmítat brzy a deterministicky (typicky HTTP 429 nebo 503), místo aby nechal požadavky nabíhat do nekonečné fronty. Právě k tomu slouží tento ukázkový zdrojový kód: lehké Concurrency-Gate (semafor) plus timeouty, které lze integrovat do existujících REST endpointů – nezávisle na tom, zda používáte Indy, WebBroker, Horse nebo vlastní HTTP vrstvu.
Architektonická myšlenka: Concurrency-Gate před „nákladnou částí“
Základní myšlenka je jednoduchá: před nákladnou částí (přístup do databáze, složité reporty, velké JSON odpovědi) se rezervuje token ze semaforu. Není-li token volný, vrátí se ihned kontrolovaná odpověď. Důležité je: tuto bránu je nutné spolehlivě uvolnit (try/finally) a musí být umístěna do cesty kódu, která je skutečně nákladná – ne pouze na úplném začátku request-handleru, pokud se pak stejně spustí parser/router/autentizace.
Tím se zátěž neodstraní, ale nasměruje: server zpracuje méně požadavků současně, zato s stabilnější latencí. V individuálních podnikových aplikacích je to většinou cennější než občasné nejlepší časy ve syntetických benchmarcích.
Ukázka zdrojového kódu: omezovač požadavků s timeoutem, 429/503 a telemetrickými hooky
Následující Delphi-kód implementuje Concurrency-Gate jako třídu TRestRequestGate. Založeno na TSemaphore (z System.SyncObjs; semafor je čítač pro omezené současné přístupy). Volání gate buď vrátí „Lease“-objekt (podobně RAII: uvolnění v destruktoru), nebo se rozhodne pro okamžitou odpověď při přetížení. Navíc jsou zde hooky pro logování/monitoring, abyste v provozu viděli, proč byly požadavky zamítnuty.
unit RESTRequestGate;
interface
uses
System.SysUtils,
System.Classes,
System.SyncObjs,
System.Diagnostics;
type
// Minimální kontext pro logování/trace; lze např. rozšířit o uživatele/trasu.
TRESTGateContext = record
RequestId: string;
Route: string;
RemoteIp: string;
end;
TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);
// Hook pro provozní telemetrii (např. do souboru, Syslog, Prometheus exportéru, atd.)
TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
Decision: TRESTOverloadDecision;
WaitedMs: Integer;
InFlight: Integer);
// Lease-objekt: uvolnění tokenu v 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: žádné čekání, okamžitě 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;
// Nejprve snížit čítač, potom uvolnit 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í být > 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 při TimeoutMs > 0: cílené čekání, ale omezit.
Decision := odRejectedTimeout;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
else
begin
// wrAbandoned/chybové případy: konzervativně zamítnout
Decision := odRejectedBusy;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
end;
end;
end.Účel: stabilita pod zátěží místo „všechno najednou“
Pomocí MaxInFlight definujete, kolik Requests současně může vstoupit do „nákladné části“. To není záměrně „počet jader CPU“, ale provozní parametr. U endpointů zatížených databází je často rozumné nastavit MaxInFlight v relaci k poolu připojení k DB (například Pool = 20, MaxInFlight = 12 až 16), aby každý Request nezablokoval připojení a následná vlákna nezačala čekat.
Okrajové podmínky a úskalí
- Try/Finally je povinnost: Lease musí být garantovaně uvolněn. Pokud v endpointu dojde k výjimkám, brána jinak „protéká“ a server zůstane trvale v stavu „busy“.
- Rozumně zvolit timeout:
TimeoutMs=0je tvrdý limit (ihned odmítnout). Krátký timeout (typicky 50 až 150 ms) vyhladí špičky, aniž by vytvářel skutečné fronty. - Bránu neumisťujte příliš brzy: Autentizace (například Bearer/JWT) nebo routing může být výhodné; semafor by měl zasáhnout před skutečně nákladnou částí. Naopak: pokud je autentizace náročná (např. vůči externímu identity systému), musí být i ta omezena.
- 429 vs 503: HTTP 429 („Too Many Requests“) se hodí, pokud klienti mají cíleně retryovat. 503 („Service Unavailable“) se hodí, pokud služba dočasně obecně není schopna přijímat požadavky smysluplně. V obou případech je doporučený
Retry-After-header.
Integrace do REST-handleru: Indy/WebBroker/Horse pragmaticky
Snippet je záměrně frameworkově neutrální. Potřebujete jen místo, kde Requests „procházejí“. Typicky je to globální singleton nebo gate na skupinu rout (například „/reports“ menší, „/health“ bez gate). Níže příkladné začlenění jako vzor:
- Naplňte kontext (RequestId, Route, RemoteIp)
TryAcquires krátkým timeoutem- Při zamítnutí ihned zapsat Response (429/503) a ukončit
- Lease zůstává v rozsahu až do dokončení nákladné části
V Horse (Middleware) je gate umístěno blízko skupiny rout. Ve WebBrokeru můžete pracovat v příslušném Action-Handleru. U Indy záleží na tom, zda máte pro každý Request vlastní vlákno; gate funguje i tak, pokud jsou nákladné úseky jasně ohraničené.
High Performance REST Server Delphi: Overload-Antworten, die Clients nicht „vergiften“
Odpovědi při přetížení jsou víc než status kódy. Pokud klienti při 429/503 agresivně okamžitě znovu posílají, vznikne retry-storm. V heterogenních systémech (mobilní aplikace, C# Services, legacy klienti) pomáhá konzistentní chování:
- Retry-After: například 1 až 3 sekundy, podle endpointu. To je jasný takt.
- Krátké tělo: Malé JSON jako
{"error":"server_busy","requestId":"..."}stačí. Velké error-objekty opět stojí CPU a šířku pásma. - Health-Endpoint bez omezení: Monitoring by měl i pod zátěží poskytovat informace (případně s příznakem „degraded“).
Pokud provozujete před aplikací reverse proxy jako nginx: sladit tam timeouts a buffering. Proxy může odlehčit (TLS-Termination, Keep-Alive), ale také přemístit zátěž (např. bufferovat velké Request-Bodies). V provozu je důležité, aby limity byly konzistentní: Proxy-Timeout > App-Timeout, jinak klienti uvidí „Gateway Timeout“, i když by aplikace správně odmítla.
Threading, DB-Pools a Keep-Alive: Kde v praxi dochází k přetížení
Gate řeší problém „příliš mnoho současně“, ale automaticky nezabraňuje tomu, že jediný požadavek nadměrně vázá prostředky. Tři typické body zlomu z Delphi-projektů vznikají právě na rozhraních mezi threadingem, databází a HTTP připojeními:
- Požadavek blokuje několik omezených zdrojů: Nejdříve DB připojení, pak externí HTTP volání, poté přístup k souboru. Pokud se to všechno děje ve stejném vlákně požadavku, čas blokace se násobí. Gate sice omezuje paralelitu, ale propustnost dramaticky klesne. Vyplatí se závislosti oddělit (např. externí volání asynchronně, předpočítávání přes Job-Queue).
- BDE-náhrada s nativním připojením-Pooling a transakce: BDE-Ablosung mit nativer Anbindung může poolovat připojení, ale „dlouhá“ transakce (např. protože tvorba JSON nebo business checky probíhají mezi StartTransaction a Commit) drží spojení zbytečně. Správná praxe je omezit transakci co nejvíce kolem vlastních SQL příkazů a mimo transakci provádět serializaci nebo validace, pokud to doménově dává smysl.
- HTTP Keep-Alive jako skrytý žrout paměti: Keep-Alive snižuje handshake, ale při mnoha neaktivních klientech může vést k příliš mnoha otevřeným socketům. Zejména u Windows- a Linux-Services se pak neprojeví „CPU nahoru“, ale „Handles/FDs plné“ nebo zvýšenou pamětí kvůli bufferům. Pomohou jasné idle-timeouty na serveru i na reverse proxy a limit na klientskou IP, pokud to prostředí dovolí.
Důsledek: MaxInFlight není statická hodnota. Závisí na vaší nejpomalejší, nejvíce omezené zdroji (DB, externí systémy, úložiště) a na tom, jak moc požadavek tyto zdroje „drží“.
Výkonnostní páky vedle Gate: JSON, DB a I/O nemíchat
Gate stabilizuje, ale nenahrazuje čistou ekonomiku endpointu. Tři brzdy u Delphi REST-serverů se opakovaně objevují:
- Sestavování JSON se zbytečnými mezilehlými řetězci: Často vzniká zátěž kvůli mnoha dočasným Unicode-řetězcům. Kde je to možné, stavět orientovaně na streamu (Writer/Stream) místo obřích mezilehlých objektů, zejména u endpointů vracejících seznamy.
- Přístup k databázi „pro položku“: N+1-queries a per-row lookups jsou klasika. Lepší je cílené použití JOINů, batch dotazů a server-side agregace. Při velmi velkých výsledcích se vyplatí navíc stránkování se stabilním řazením (aby stránky „neskákaly“).
- Blokující I/O ve vlákně požadavku: Přístupy k souborům nebo externí HTTP volání by měly být buď striktně omezeny, nebo přesunuty do asynchronní pipeline. Jinak blokujete drahá vlákna čekáním.
Pro rostoucí digitální podniková řešení je to často klíčový bod: Endpoint byl „rychle“ doplněn a funguje, dokud nepřijdou reálné zátěže a objemy dat. Tehdy se ukáže, zda byly architektonické hranice jasně vymezeny (vrstva přístupu k datům, caching, bulk strategie, jasné timeouts).
Debugging a provoz: Co byste měli měřit
Hook OnEvent je záměrně jednoduchý. V praxi byste měli minimálně zaznamenávat následující hodnoty:
- InFlight (aktuální paralelita na Gate)
- WaitedMs (kolik „čekání ve frontě“ povolíte)
- Decision (accepted/busy/timeout)
- Route/RemoteIp (hrubá analýza příčin, aniž by se ignorovala ochrana osobních údajů)
Tím získáte signál, zda jsou limity příliš přísné (příliš mnoho 429) nebo příliš volné (vysoké WaitedMs, rostoucí latence). A uvidíte, zda jednotlivé trasy dominují. Pro Windows- a Linux-Services je to v provozu rozhodující: bez telemetrie se problém s výkonem rychle stane hrou odhadů mezi sítí, databází, proxy a aplikací.
Neobvyklé, ale mimořádně užitečné: „WaitedMs“ jako včasný varovný ukazatel
Mnoho týmů sleduje pouze Response-Time a CPU. WaitedMs je často lepší indikátor, protože ukazuje, že requesty už čekají před vlastní prací. Pokud WaitedMs roste, zatímco CPU zůstává na mírném stupni, úzkým místem často není CPU, ale pool (DB připojení), zámek v business logice nebo externí downstream služba. To šetří čas při analýze příčin, protože můžete cíleněji hledat směrem „Pool/Lock/I/O“ místo „optimalizace kompilátoru“.
Varianty: Pro-Route-Gates, priority a „Fast Lane“
Jedna brána pro vše je jednoduchá, ale ne vždy ideální. Smysluplné varianty:
- Brána pro skupinu tras: „/reports“ přísná, „/api/orders“ mírná, „/health“ otevřená. Tak zabráníte, aby nákladné reportovací requesty vytlačily klíčové procesy.
- Fast Lane pro Admin/Monitoring: Samostatná brána s nízkou paralelitou, aby provozní zásahy byly možné i při zatížení.
- Limity založené na rozpočtu: Pokud se velikosti odpovědí výrazně liší, může pomoci i bytový rozpočet (např. maximálně X MB současně při generování). To je složitější, ale u velkých stahování realistické.
Důležité: priorizace se rychle stane politickou („můj endpoint je důležitější“). Technicky stabilní to zůstane, pokud jsou priority vázány na procesy (např. záznam zakázek před reportingem), nikoli na role nebo oddělení.
Závěr: Vyplatí se brána — a kde tento přístup selhává?
Concurrency-Gate je pragmatický stavební blok pro High Performance REST server v Delphi, protože umožňuje kontrolovat přetížení a udržet systémy stabilní při špičkovém zatížení. Zvláště se vyplatí, pokud máte databází vázané endpointy, pokud před serverem stojí reverzní proxy nebo pokud více klientů (legacy, portály, služby) vytváří zatížení vlnově.
Meze jsou jasné: pokud je práce na jeden request příliš nákladná (neefektivní dotazy, velké JSON objekty, blokující externí systémy), brána pouze zamaskuje symptomy. V tom případě je potřeba upravit přístup k datům, cachingové strategie, time-outy a případně zavést asynchronní zpracování (queue/job systém). Jako bezpečnostní pás v provozu je brána však často rozdíl mezi „krátce použitelné“ a „zcela nepoužitelné“.
Pokud chcete zavést chování proti přetížení do existující Delphi REST-API und REST-Server nebo vyvážit limity se databázovými a proxy time-outy: projednejte projekt nebo modernizační záměr s Net-Base.
V odborném prostředí hrají důležitou roli také thread-pool Delphi a Http 429 Too Many Requests, pokud musí integrace, datové toky a další vývoj spolu hladce fungovat.
Další krok
Když se z tématu stane reálný projekt, měly by být architektura, stávající systém a provoz včas posuzovány společně.
Podporujeme nejen při jednotlivých otázkách, ale i v případě, že se z útržků zdrojového kódu, legacy témat nebo nápadů na portál má vyvinout robustní podnikový projekt.
- Současný stav, cílový stav a technická rizika jsou hodnoceny společně.
- REST, přístup k datům, portály a nasazení nebudou odkládány na později.
- Vidíte včas, která cesta je ekonomicky i provozně životaschopná.