Från magasinets tema till projektpraxis
Passande tjänste- och tekniksidor för inlägget
Varför „High Performance“ hos REST i Delphi ofta misslyckas på grund av samtidighet
En High Performance REST Server Delphi är i praktiken sällan begränsad av ren CPU-tid per Request, utan av okontrollerad samtidighet: för många samtidiga Requests, för många samtidiga databas-queries eller blockerande I/O (fil, nätverk, databas). Resultatet upplevs då inte som ”lite långsammare”, utan som en kedjereaktion: fler trådar, längre köer, connection-pool-kollaps, ökande latenser, timeouts på klientsidan och i slutändan en server som visserligen fortfarande ”lever”, men inte längre levererar stabila svar.
Motmedlet är ingen enskild trick, utan ett medvetet överbelastningsbeteende: När servern når sina gränser måste den tidigt och deterministiskt neka (typiskt HTTP 429 eller 503), istället för att låta Requests köa i en oändlig väntetid. Just för detta är denna källkodssnutt avsedd: ett lättviktigt Concurrency-Gate (Semaphore) plus Timeouts, som kan integreras i befintliga REST-endpoints – oavsett om ni använder Indy, WebBroker, Horse eller ett eget HTTP-lager.
Arkitekturidé: Concurrency-Gate före den „kostsamma delen”
Grundidén är enkel: Innan den kostsamma delen (databasåtkomst, komplexa rapporter, stora JSON-svar) reserveras en token från en Semaphore. Finns ingen token ledig ges omedelbart ett kontrollerat svar. Viktigt är: Detta Gate måste pålitligt frigöras (try/finally), och det måste sitta i den kodväg som verkligen är dyr – inte bara i början av Request-handlern, när det ändå följer parser/router/autentisering efteråt.
På så sätt optimeras inte lasten bort, utan kanaliseras: Servern svarar på färre Requests samtidigt, men med stabilare latenser. I skräddarsydda företagsapplikationer är det oftast mer värdefullt än sporadiska rekordtider i syntetiska benchmarks.
Källkodssnutt: Request-Limiter med Timeout, 429/503 och Telemetri-Hooks
Följande Delphi-kod implementerar ett Concurrency-Gate som klassen TRestRequestGate. Det bygger på TSemaphore (från System.SyncObjs; en Semaphore är en räknare för begränsade samtidiga åtkomster). Gate-anropet returnerar antingen ett „Lease”-objekt (RAII-liknande: frigöring i destruktorn) eller bestämmer sig för ett omedelbart överbelastningssvar. Dessutom finns hooks för logging/monitoring, så att ni i drift kan se varför Requests avvisades.
unit RESTRequestGate;
interface
uses
System.SysUtils,
System.Classes,
System.SyncObjs,
System.Diagnostics;
type
// Minimal kontext för loggning/spårning; kan t.ex. utökas med användare/route.
TRESTGateContext = record
RequestId: string;
Route: string;
RemoteIp: string;
end;
TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);
// Hook för drifttelemetri (t.ex. fil, syslog, Prometheus-exporter, etc.)
TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
Decision: TRESTOverloadDecision;
WaitedMs: Integer;
InFlight: Integer);
// Lease-objekt: frigör token i destruktorn.
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 väntetid, omedelbart 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;
// Minska först räknaren, frigör sedan 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åste vara > 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 vid TimeoutMs > 0: avsiktlig väntan men begränsad.
Decision := odRejectedTimeout;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
else
begin
// wrAbandoned/fel: avvisa konservativt.
Decision := odRejectedBusy;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
end;
end;
end.Syfte: Stabilitet under belastning istället för „allt samtidigt“
Med MaxInFlight definierar du hur många requests som samtidigt får gå in i den „kostsamma delen“. Det är medvetet inte „antal CPU-kärnor“, utan en driftsparameter. För databasintensiva Endpoints är det ofta lämpligt att sätta MaxInFlight i relation till DB-Connection-Poolen (till exempel Pool = 20, MaxInFlight = 12 till 16), så att inte varje request blockerar en anslutning och därefter fler trådar dras in.
Randvillkor och fallgropar
- Try/Finally är obligatoriskt: Leasen måste garanteras frigöras. Om du får Exceptions i endpointen blir annars Gate „läckande“ och servern förblir permanent „busy“.
- Välj timeout med omsorg:
TimeoutMs=0är en hård gräns (avvisa omedelbart). En kort timeout (typiskt 50 till 150 ms) jämnar ut toppar utan att bygga upp riktiga köer. - Gate inte för tidigt: Autentisering (till exempel Bearer/JWT) eller routing kan vara lämpligt; semaforen bör placeras före den verkligt kostsamma sektionen. Omvänt: om autentisering är dyr (t.ex. mot ett externt identity-system) måste även den begränsas.
- 429 vs 503: HTTP 429 („Too Many Requests“) passar när klienter avsiktligt ska göra retry. 503 („Service Unavailable“) passar när tjänsten temporärt generellt inte kan ta emot förfrågningar på ett meningsfullt sätt. I båda fallen är en
Retry-After-header rekommenderbar.
Integration i REST-Handler: Indy/WebBroker/Horse pragmatiskt
Snippeten är avsiktligt ramverksneutral. Det behövs bara en plats där requests „passerar igenom“. Typiskt är ett globalt singleton eller ett Gate per routgrupp (till exempel „/reports“ mindre, „/health“ utan Gate). Exempel på införande som mönster:
- Fyll kontexten (RequestId, Route, RemoteIp)
TryAcquiremed kort timeout- Vid avslag skriv omedelbart en Response (429/503) och avsluta
- Leasen lever i Scope tills efter den kostsamma delen
I Horse (Middleware) ligger Gate nära en routgrupp. I WebBroker kan du arbeta i respektive Action-Handler. För Indy beror det på om du har en tråd per request; Gateet verkar ändå så länge de kostsamma avsnitten är tydligt avgränsade.
High Performance REST Server Delphi: Overload-svar som inte „förgiftar“ klienter
Svar vid överbelastning är mer än statuskoder. Om klienter vid 429/503 aggressivt skickar igen omedelbart får du en retry-storm. I heterogena systemlandskap (mobilappar, C# Services, legacy-klienter) hjälper ett konsekvent beteende:
- Retry-After: till exempel 1 till 3 sekunder, beroende på Endpoint. Det är en tydlig taktgivare.
- Kort body: En liten JSON som
{"error":"server_busy","requestId":"..."}räcker. Stora error-objekt kostar åter CPU och bandbredd. - Health-Endpoint utan nedreglering: Monitoring ska även vid last ge utsagor (eventuellt med „degraded“-flagga).
Om du kör en Reverse Proxy som nginx framför: stäm av Timeouts och Buffering där. En proxy kan avlasta (TLS-Termination, Keep-Alive), men också flytta belastning (till exempel buffra stora Request-Bodies). I drift är det väsentligt att begränsningarna är konsekventa: Proxy-Timeout > App-Timeout, annars ser klienterna „Gateway Timeout“, även om appen korrekt hade avvisat.
Trådning, DB-pooler och Keep-Alive: Var det brister i praktiken
Gate löser problemet med „för många samtidigt“, men det förhindrar inte automatiskt att en enskild request binder oproportionerligt mycket resurser. Tre typiska brytpunkter från Delphi-projekt uppstår precis i gränssnitten mellan trådning, databas och HTTP-anslutningar:
- En request blockerar flera knappa resurser: Först en DB-anslutning, sedan ett externt HTTP-anrop, sedan åtkomst till filsystemet. Om allt detta sker i samma requesttråd multipliceras blockeringstiden. Gate begränsar visserligen parallelliteten, men genomströmningen sjunker drastiskt. Här är det värt att lösgöra beroenden (t.ex. göra externa anrop asynkront, förberäkning via jobbkö).
- BDE-Ablosung mit nativer Anbindung-Pooling und Transaktionen: BDE-Ablosung mit nativer Anbindung kan poola anslutningar, men en „lång“ transaktion (t.ex. för att JSON-generering eller affärsvalideringar ligger mellan StartTransaction och Commit) håller anslutningen onödigt länge. En ren praxis är att hålla transaktionen så snäv som möjligt kring de faktiska statements och, om det går ur domänsynpunkt, serialisera eller validera utanför transaktionen.
- HTTP Keep-Alive som dold minnesätare: Keep-Alive minskar handskakningar men kan vid många inaktiva klienter leda till för många öppna sockets. Speciellt för Windows- och Linux-Services ser man då inte „CPU upp“, utan „Handles/FDs fulla“ eller RAM som växer pga. buffrar. Här hjälper tydliga idle-timeouts i servern och i reverse-proxyn samt en gräns per klient-IP om miljön tillåter.
Konsekvensen: MaxInFlight är inget statiskt värde. Det beror på din långsammaste, mest knappa resurs (DB, externa system, storage) och på hur väl en request „håller ihop“ dessa resurser.
Prestandaåtgärder utöver Gate: blanda inte JSON, DB och I/O
Gate stabiliserar, men ersätter inte en ren endpoint-ekonomi. Tre återkommande bromsar i Delphi REST-servrar dyker upp gång på gång:
- JSON-byggande med onödiga mellansträngar: Ofta uppstår belastning genom många temporära Unicode-strängar. Bygg i möjligaste mån streaming-orienterat (writer/stream) istället för stora mellanobjekt, särskilt vid lista-endpoints.
- Databasåtkomst „per item“: N+1-queries och per-rad-uppslag är klassikern. Bättre är riktade joins, batch-queries och serversidig aggregering. Vid mycket stora resultat lönar sig också paginering med stabil sortering (så att sidorna inte „hoppar“).
- Blockerande I/O i requesttråden: Filåtkomst eller externa HTTP-anrop bör antingen strikt begränsas eller flyttas till en asynkron pipeline. Annars blockerar ni dyra trådar för „väntan“.
För växande digitala företagslösningar är detta ofta knäckfrågan: En endpoint lades till „snabbt“ och fungerar tills verklig last och datavolymer kommer. Då visar det sig om arkitekturgränserna är tydligt dragna (dataåtkomstlager, caching, bulk-strategier, tydliga timeouts).
Debugging och drift: Vad ni bör mäta
Hooken OnEvent är medvetet enkel. I praktiken bör ni åtminstone samla in följande värden:
- InFlight (aktuell parallellitet vid Gate)
- WaitedMs (hur mycket köbildning ni tillåter)
- Decision (accepted/busy/timeout)
- Route/RemoteIp (grov orsaksanalys, utan att ignorera dataskyddet)
Det ger er en signal om huruvida gränserna är för stränga (för många 429) eller för lösa (höga WaitedMs, stigande latenser). Och ni ser om enskilda rutter dominerar. För Windows- och Linux-tjänster är detta avgörande i vardagen: Utan telemetri blir ett prestandaproblem snabbt ett gissningsspel mellan nätverk, databas, proxy och applikation.
Ovanligt, men extremt användbart: „WaitedMs“ som tidig varningsindikator
Många team tittar bara på responstid och CPU. WaitedMs är ofta den bättre indikatorn, eftersom den visar att requests redan väntar före det verkliga arbetet. Om WaitedMs stiger medan CPU förblir måttlig är den knappaste resursen ofta inte CPU utan en pool (DB-anslutningar), ett lås i affärslogiken eller en extern downstream-tjänst. Det sparar tid vid orsaksanalys eftersom ni kan söka mer riktat mot „Pool/Lock/I/O“ istället för „kompilatoroptimering“.
Varianter: Gate per rutt, prioriteringar och „Fast Lane“
En gate för allt är enkelt, men inte alltid idealiskt. Meningsfulla varianter:
- Gate per ruttgrupp: „/reports“ strikt, „/api/orders“ måttlig, „/health“ öppen. Så förhindrar ni att kostsamma rapportförfrågningar tränger undan kärnprocesser.
- Fast Lane för admin/övervakning: Separat gate med låg parallellitet, så att driftåtgärder också är möjliga under belastning.
- Budgetbaserade gränser: När responsstorlekar varierar kraftigt kan ett byte-budget dessutom hjälpa (t.ex. maximalt X MB samtidigt under generering). Det är mer komplext, men realistiskt vid stora nedladdningar.
Viktigt: Prioritering blir snabbt politisk („min Endpoint är viktigare“). Tekniskt stabilt förblir det om prioriteringar kopplas till processer (t.ex. orderinmatning före rapportering), inte till roller eller avdelningar.
Slutsats: Är ett Concurrency-Gate värt det – och när brister angreppssättet?
Ett Concurrency-Gate är en pragmatisk byggsten för en High Performance REST-server i Delphi, eftersom det gör överbelastning hanterbar och håller era system stabila vid peak-last. Det lönar sig särskilt om ni har databaskopplade endpoints, om en reverse proxy står framför eller om flera klienter (Legacy, portaler, tjänster) skapar belastning i vågor.
Gränserna är tydliga: Om det verkliga arbetet per request är för dyrt (ineffektiva queries, stora JSON-objekt, blockerande externa system) maskerar Gate bara symtomen. Då måste dataåtkomst, cachningsstrategier, timeouter och eventuellt asynkron bearbetning (kö-/jobbsystem) åtgärdas. Som säkerhetsbälte i drift är Gate ofta skillnaden mellan „kort segt“ och „helt obrukbart“.
Om ni vill införa overload-beteende i en befintlig Delphi REST-API och REST-Server eller balansera gränser i samklang med databas- och proxy-timeouter: diskutera projekt eller moderniseringsinitiativ med Net-Base.
I det tekniska sammanhanget spelar även thread-pool Delphi och HTTP 429 Too Many Requests en viktig roll när integrationer, dataflöden och vidareutveckling måste samspela väl.
Diskutera projekt eller moderniseringsinitiativ med Net-Base.
Nästa steg
När ett ämne blir ett verkligt projekt bör arkitektur, befintliga system och drift behandlas gemensamt redan i ett tidigt skede.
Vi stöder inte bara vid enstaka frågor, utan även när kodsfragment, legacy-frågor eller portalidéer ska utvecklas till ett robust företagsprojekt.
- Nuläge, målbild och tekniska risker bedöms tillsammans.
- REST, dataåtkomst, portaler och utrullning skjuts inte upp som sena följder.
- Ni ser tidigt vilken väg som är ekonomiskt och driftsmässigt bärkraftig.