Frå magasinetema til prosjektpraksis
Passande teneste- og tekniske sider til innlegget
Kvifor «høg ytelse» hos REST i Delphi ofte feilar på grunn av samtidighet
Ein «høg ytelse» REST Server Delphi er i praksis sjeldan avgrensa av rein CPU‑tid per Request, men av ukontrollert samtidighet: for mange samtidige Requests, for mange samtidige database‑Queries eller blokkerande I/O (fil, nettverk, database). Resultatet verkar då ikkje som «eit lite meir tregt», men som ein kjedereaksjon: fleire trådar, lengre venterekker, Connection‑Pool‑kollaps, aukande latenser, Timeouts på klientsida og til slutt ein server som framleis «lever», men som ikkje lenger leverer stabile svar.
Motmiddelet er ikkje ein enkelttriks, men ein medviten overbelastingsåtferd: Når serveren når grensene sine, må han tidleg og deterministisk avvise (vanlegvis HTTP 429 eller 503), i staden for å la Requests hamne i ein uendeleg ventekø. Nettopp for dette er denne Source-Schnipsel meint: eit lettvekt Concurrency‑Gate (Semaphore) pluss Timeouts, som kan integrerast i eksisterande REST-endepunkt — uavhengig av om du brukar Indy, WebBroker, Horse eller eit eige HTTP‑lag.
Arkitekturidé: samtidigheitsport foran den «kostbare delen»
Grunntanken er enkel: Før den kostbare delen (database-tilgang, komplekse rapportar, store JSON-svar) blir eit token reservert frå ein Semaphore. Er inga token fri, kjem det umiddelbart eit kontrollert svar. Viktig er: denne porten må bli påliteleg frigitt (try/finally), og han må liggje i den kodevegen som verkeleg er kostbar – ikkje berre heilt i starten av Request-Handleren, når det likevel følgjer parser/router/autentisering etterpå.
Slik blir ikkje lasten «optimert bort», men kanalisert: Serveren svarar på færre Requests samtidig, men med stabilare latenser. I individuelle bedriftsapplikasjonar er dette som regel meir verdifullt enn sporadiske besttider i syntetiske benchmarkar.
Source-Schnipsel: Request-Limiter med Timeout, 429/503 og Telemetrie-Hooks
Følgjande Delphi-kode implementerer eit Concurrency‑Gate som klasse TRestRequestGate. Han er basert på TSemaphore (frå System.SyncObjs; ein Semaphore er ein teljar for avgrensa samtidige tilgangar). Kall på porten leverer anten eit «Lease»-Objekt (RAII-liknande: frigjering i destruktøren) eller avgjer seg for eit omgåande overbelastingssvar. I tillegg finst det hooks for logging/monitoring, slik at du i drift kan sjå kvifor Requests vart avvist.
unit RESTRequestGate;
interface
uses
System.SysUtils,
System.Classes,
System.SyncObjs,
System.Diagnostics;
type
// Minimal kontekst for logging/tracing; kan t.d. utvidast med brukar/Route.
TRESTGateContext = record
RequestId: string;
Route: string;
RemoteIp: string;
end;
TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);
// Hook for drifts-telemetri (t.d. til fil, syslog, Prometheus-exporter osv.)
TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
Decision: TRESTOverloadDecision;
WaitedMs: Integer;
InFlight: Integer);
// Lease-objekt: frigjering av token i destruktoren.
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: ingen ventetid, straks 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;
// Fyrst teljaren ned, deretter frigjering av semaforen.
TInterlocked.Decrement(FInFlightCounter^);
FSemaphore.Release;
end;
{ TRESTRequestGate }
constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
inherited Create;
if AMaxInFlight <= 0 then
raise EArgumentException.Create(‚AMaxInFlight må vere > 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 ved TimeoutMs > 0: målretta venting, men avgrensa.
Decision := odRejectedTimeout;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
else
begin
// wrAbandoned/feiltilfelle: avvise konservativt
Decision := odRejectedBusy;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
end;
end;
end.
Formål: stabilitet under last i staden for «alt samtidig»
Med MaxInFlight definerer du kor mange Requests som samtidig kan gå inn i den «krevjande delen». Dette er medvite ikkje «Antall CPU-kjerner», men ein driftsstorleik. For databasalastede endpoints er det ofte fornuftig å setje MaxInFlight i forhold til DB-tilkoblingspoolen (til dømes Pool = 20, MaxInFlight = 12 til 16), slik at ikkje kvar Request blokkerer ei tilkopling og følgjande trådar må vente.
Randvilkår og fallgruver
- Try/Finally er obligatorisk: Leasen må garanterast frigjort. Om du har unntak i endepunktet, blir elles Gate «lekk» og serveren kan stå permanent som «busy».
- Vel timeout med omhug:
TimeoutMs=0er ein hard grense (avvis umiddelbart). Ein kort timeout (vanlegvis 50 til 150 ms) jamnar ut toppar utan å bygge opp reelle køar. - Ikke set Gate for tidleg: Autentisering (til dømes Bearer/JWT) eller routing kan vere hensiktsmessig; semaforen bør gripe inn før den verkeleg kostbare delen. Om autentisering derimot er kostbar (t.d. mot eit eksternt Identity-System), må òg den avgrensast.
- 429 vs 503: HTTP 429 («Too Many Requests») passar godt når klientar skal retrye målretta. 503 («Service Unavailable») passar når tenesta midlertidig generelt ikkje er i stand til å ta imot førespurnader på ein meiningsfull måte. I begge tilfelle er ein
Retry-After-header å anbefale.
Integrasjon i REST-handler: Indy/WebBroker/Horse pragmatisk
Snippetet er medvite rammeverksnøytralt. Du treng berre eitt stad der Requests «går gjennom». Typisk er eit globalt singleton eller eit Gate per rute-gruppe (til dømes «/reports» mindre, «/health» utan Gate). Følgjande er eit døme på innbinding som mønster:
- Fyll kontekst (RequestId, Route, RemoteIp)
TryAcquiremed kort timeout- Ved avslag: skriv straks Response (429/503) og avslutt
- Leasen lever i scope til etter den kostbare delen
I Horse (Middleware) ligg Gate nært ei rute-gruppe. I WebBroker kan du arbeide i den aktuelle Action-Handleren. I Indy avheng det av om du har ein tråd per Request; Gate verkar likevel så lenge dei kostbare delane er klart avgrensa.
High Performance REST Server Delphi: Overload-svar som ikkje «forgiftar» klientar
Overlast-svar er meir enn statuskodar. Om klientar ved 429/503 aggressivt sender på nytt med ein gong, får du eit retry-storm. I heterogene systemlandskap (mobile appar, C# Services, legacy-klientar) bidrar ein konsekvent åtferd:
- Retry-After: til dømes 1 til 3 sekund, avhengig av endpoint. Dette er ein tydeleg taktgeber.
- Kort body: Eit lite JSON som
{"error":"server_busy","requestId":"..."}er nok. Store error-objekt kostar igjen CPU og bandbreidde. - Health-Endpoint utan drossling: Overvaking skal også under last kunne gi svar (eventuelt med «degraded»-flag).
Dersom du køyrer ein reverse proxy som nginx framom: synkroniser timeouts og buffering der. Ein proxy kan avlaste (TLS-Termination, Keep-Alive), men òg flytte eller utsette lasten (til dømes bufre store Request-Bodies). I drift tel det at grensene er konsistente: Proxy-Timeout > App-Timeout, elles ser klientane «Gateway Timeout», sjølv om appen ville ha avvist ryddig.
Threading, DB-Pools og Keep-Alive: kvar det tippar over i praksis
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:
- Ein Request blockiert mehrere knappe Ressourcen: Erst 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-avløysing med native tilkopling-Pooling und Transaktionen: BDE-Ablosung mit nativer Anbindung kan poole tilkoblingar, 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“.
Ytelsesgrep ved sida av Gate: ikkje blande JSON, DB og I/O
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 (grove årsaksanalyse, utan å ignorere personvern)
Det gir deg eit signal om grensene er for strenge (for mange 429) eller for løse (høg WaitedMs, aukande latens). Og du ser om enkelte ruter dominerer. For Windows- og Linux-Services er dette i kvardagen avgjerande: Uten telemetri blir eit ytelsesproblem raskt eit gjetteleik mellom nettverk, database, proxy og applikasjon.
Uvanleg, men ekstremt nyttig: „WaitedMs“ som tidleg varselindikator
Mange team ser berre på responstid og CPU. WaitedMs er ofte ein betre indikator, fordi han viser at requests allereie ventar før det eigentlege arbeidet startar. Stig WaitedMs medan CPU held seg moderat, er den knappaste ressursen ofte ikkje CPU-en, men ein pool (DB-tilkoplingar), eit lås i forretningslogikken eller ein ekstern downstream-teneste. Dette sparar tid i årsaksanalyse, fordi du kan søkje meir målretta mot «Pool/Lock/I/O» i staden for «kompilatoroptimalisering».
Variantar: Per-Route-Gates, prioriteringar og „Fast Lane“
Eitt gate for alt er enkelt, men ikkje alltid ideelt. Sentrale variantar:
- Gate per rute-gruppe: „/reports“ streng, „/api/orders“ moderat, „/health“ open. Slik forhindrar du at kostbare rapport-forespørslar trengjer ut kjerneprosessar.
- Fast Lane for Admin/Monitoring: Eige gate med låg parallellitet, slik at driftsoperasjonar framleis er moglege under last.
- Budsjettbaserte grenser: Når responsstorleikar varierer mykje, kan eit byte-budsjett i tillegg hjelpe (t.d. maksimalt X MB samtidig under generering). Dette er meir komplekst, men realistisk ved store nedlastingar.
Viktig: Prioritering blir lett politisk («mitt endepunkt er viktigare»). Teknisk stabilt blir det dersom prioritetar er kopla til prosessar (f.eks. ordreopptak før rapportering), ikkje til roller eller avdelingar.
Konklusjon: Løner gate seg — og kvar sviktar tilnærminga?
Eit Concurrency-Gate er ein pragmatisk komponent for ein high-performance REST-server i Delphi, fordi det gjer overload kontrollerbart og held systema dine stabile under peak-last. Det løner seg særleg om du har databasebundne endepunkt, om ein reverse proxy står framfor, eller om fleire klientar (Legacy, portalar, Services) genererer last i bølgjer.
Grensene er klare: Når det eigentlege arbeidet per forespørsel er for kostbart (ineffektive spørringar, store JSON-objekt, blokkerande eksterne system), skjuler gateet berre symptom. Då må dataåtkomst, caching-strategiar, timeouts og eventuelt asynkron behandling (Queue/Job-System) følgjast opp. Som tryggingsreim i drift er gateet ofte skilnaden mellom «litt treigt» og «fullstendig ubrukelig».
Om du vil implementere overload-handtering i ein eksisterande Delphi REST-API og REST-Server eller vil balansere grenser mot database- og proxy-timeouts på ein ryddig måte: diskuter prosjekt eller moderniseringsprosjekt med Net-Base.
I fagleg samanheng spelar òg Thread-Pool Delphi og Http 429 Too Many Requests ei viktig rolle når integrasjonar, dataflytar og vidareutvikling må samspela tett.
Diskuter prosjekt eller moderniseringsprosjekt med Net-Base.
Neste steg
Når temaet blir eit reelt prosjekt, bør arkitektur, eksisterande system og drift vurderast tidleg saman.
Vi støttar ikkje berre ved enkeltspørsmål, men òg når korte kildekodesnuttar, legacy-tema eller portalidéar skal utviklast til eit robust bedriftsprosjekt.
- Eksisterande tilstand, målbiletet og tekniske risikoar blir vurderast samla.
- REST, datatilgang, portalar og utrulling blir ikkje utsette til seinare som etterverknader.
- De ser tidleg kva veg som er økonomisk og driftsmessig berekraftig.