Net-Base Žurnāls

06.06.2026

Augstas veiktspējas REST serveris Delphi: pieprasījumu ierobežojumi, pavedienu kopa un sakārtota pārslogošanas uzvedība (avota koda fragments)

Augstas veiktspējas REST serveris Delphi nav ātrs tikai tāpēc, ka izmanto "ātru JSON", bet gan pateicoties kontrolētai paralelitātei, stingriem laika ierobežojumiem un tīrai pārslodzes uzvedībai. Šis raksts parāda praktiski izmantojamu Concurrency-Gate ar semaforu un 429/503 atbildēm.

06.06.2026

No žurnāla tēmas līdz projektu praksei

Atbilstošas pakalpojumu un tehniskās lapas rakstam

Kāpēc „High Performance“ ar REST platformā Delphi bieži neizdodas paralelitātes dēļ

Praksē High Performance REST serveris Delphi retāk ir limitēts tikai pēc tīras CPU laika uz pieprasījumu, bet gan nevaldāmas paralelitātes dēļ: pārāk daudz vienlaicīgu pieprasījumu, pārāk daudz vienlaicīgu datubāzes vaicājumu vai bloķējoša I/O (fails, tīkls, datubāze). Rezultāts neizskatās pēc „mazliet lēnāk“, bet drīzāk kā ķēdes reak­cija: vairāk pavedienu, garākas gaidīšanas rindas, savienojumu (connection-pool) sabrukums, pieaugošas latentēs, timeouti klienta pusē un beigās serveris, kas zwar vēl „dzīvo“, bet vairs nesniedz stabilas atbildes.

Pretlīdzeklis nav viens triks, bet apzināta pārslogošanās uzvedība: ja serveris sasniedz savas robežas, tam agrīni un deterministiski jāatsaka pieprasījumi (ierasti HTTP 429 vai 503), nevis jāļauj pieprasījumiem nonākt bezgalīgā gaidīšanas rindā. Tieši šim nolūkam domāts šis source-schnipsel: viegls Concurrency-Gate (Semaphore) ar timeoutiem, ko var integrēt esošajos REST endpointos – neatkarīgi no tā, vai izmantojat Indy, WebBroker, Horse vai savu HTTP slāni.

Architekturidee: Concurrency-Gate vor dem „teuren Teil“

Pamatideja ir vienkārša: pirms dārgās daļas (datubāzes piekļuve, sarežģīti atskaišu ģenerēšanas darbi, lielas JSON-atbildes) tiek rezervēts tokens no semafora. Ja tokens nav pieejams, tiek nekavējoties dota kontrolēta atbilde. Svarīgi: šie vārti (gate) jāatbrīvo uzticami (try/finally), un tie jāievieto tieši tajā koda ceļā, kas patiešām ir dārgs – ne tikai pašā pieprasījuma apstrādātāja sākumā, ja pēc tam tāpat vēl notiek parser/Router/autentifikācija.

Tādējādi slodzi nevis „noņem“ vai izslēdz, bet kanalizē: serveris atbild uz mazāk pieprasījumiem vienlaikus, taču ar stabilākām latentēm. Individuālajās uzņēmumu lietojumprogrammās tas bieži ir vērtīgāks nekā sporādiski labāki rezultāti sintētiskajos benchmarkos.

Source-Schnipsel: Request-Limiter mit Timeout, 429/503 und Telemetrie-Hooks

Nākamais Delphi-kods īsteno Concurrency-Gate kā klasi TRestRequestGate. Tas balstās uz TSemaphore (no System.SyncObjs; semafors ir skaitītājs ierobežotām vienlaicīgām piekļuvēm). Gate-izsaukums vai nu atgriež „Lease“ objektu (RAII-ähnlich: atbrīvošana destruktorā), vai izvēlas tūlītēju pārslogošanās atbildi. Papildus ir hooks žurnālošanai/monitoringam, lai ekspluatācijas laikā jūs redzētu, kāpēc pieprasījumi tika noraidīti.

unit RESTRequestGate;

interface

uses
System.SysUtils,
System.Classes,
System.SyncObjs,
System.Diagnostics;

type
// Minimāls konteksts žurnēšanai/izsekošanai; var, piemēram, papildināt ar lietotāja vai maršruta informāciju.
TRESTGateContext = record
RequestId: string;
Route: string;
RemoteIp: string;
end;

TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);

// Hook operacionālajai telemetrijai (piem., failā, Syslog, Prometheus eksportētājā u.c.)
TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
Decision: TRESTOverloadDecision;
WaitedMs: Integer;
InFlight: Integer);

// Lease-objekts: žetona atbrīvošana destruktorā.
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: nav gaidīšanas laika, uzreiz 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;

// Vispirms samazināt skaitītāju, pēc tam atbrīvot semaforu.
TInterlocked.Decrement(FInFlightCounter^);
FSemaphore.Release;
end;

{ TRESTRequestGate }

constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
inherited Create;
if AMaxInFlight <= 0 then raise EArgumentException.Create('AMaxInFlight jā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 pie TimeoutMs > 0: mērķtiecīga gaidīšana, bet ar ierobežojumu.
Decision := odRejectedTimeout;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
else
begin
// wrAbandoned/kļūdu gadījumi: konservatīvi noraidīt
Decision := odRejectedBusy;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
end;
end;

end.

Mērķis: stabilitāte slodzes laikā, nevis „viss vienlaikus“

Ar MaxInFlight jūs definējat, cik daudz Requests vienlaikus drīkst nonākt „resursu intensīvajā posmā“. Tas apzināti nav „Anzahl CPU-Kerne“, bet darbības parametrs. Pie datubāzes intensīviem Endpoints bieži ir jēga MaxInFlight saskaņot ar DB-Connection-Pool (piemēram Pool = 20, MaxInFlight = 12 līdz 16), lai nekatrs Request neblokētu savienojumu un pēc tam neatvilktu vēl vairāk Thread.

Nosacījumi un kritiskie punkti

  • Try/Finally ist Pflicht: Lease ir jāatbrīvo garantēti. Ja Endpoint rodas Exceptions, vārteja citādi kļūst „noplūdusi“ un serveris paliek pastāvīgi „aizņemts“.
  • Timeout sinnvoll wählen: TimeoutMs=0 ir stingrs limits (tūlītēja noraidīšana). Īss timeout (tipiski 50 līdz 150 ms) izlīdzina pīķus, neveidojot reālas gaidīšanas rindas.
  • Gate nicht zu früh: Autentifikācija (piemēram Bearer/JWT) vai routing var būt lietderīga; semaphorei jāiegriežas pirms patiešām resursus prasīgā posma. Savukārt: ja auth kļūst dārga (piem., pret ārēju Identity-System), arī to jāierobežo.
  • 429 vs 503: HTTP 429 („Too Many Requests“) der labi, ja klientiem jāsāk mērķtiecīga retry. 503 („Service Unavailable“) der, ja serviss pagaidām vispār nav spējīgs pieņemt pieprasījumus. Abos gadījumos ieteicams Retry-After header.

Integrācija REST-Handler: Indy/WebBroker/Horse pragmatisch

Šis fragments ir apzināti framework-neitrāls. Jums vajag tikai vienu vietu, caur kuru Requests „iziet“. Tipiski tas ir globāls singleton vai vārteja katrai route-grupai (piemēram „/reports“ mazāka, „/health“ bez vārtejas). Parauga ieviešana:

  • Kontext füllen (RequestId, Route, RemoteIp)
  • TryAcquire ar īsu timeout
  • Pie noraidījuma uzreiz uzrakstīt Response (429/503) un beigt
  • Lease dzīvo scope līdz pēc resursu intensīvā posma

Horse (Middleware) gadījumā vārteja ir tuvu route-grupai. WebBroker varat strādāt attiecīgajā Action-Handler. Indy gadījumā tas atkarīgs no tā, vai jums uz Request ir atsevišķs Thread; vārteja tomēr darbojas, kamēr resursu dārgie posmi ir skaidri ierobežoti.

Augstas veiktspējas REST Server Delphi: pārslogošanas atbildes, kas klientus ne „noindē“

Pārslogošanas atbildes ir vairāk nekā statuskodi. Ja Clients pie 429/503 agresīvi uzreiz mēģina vēlreiz, rodas retry-storm. Heterogēnā sistēmu ainavā (Mobile Apps, C# pakalpojumi, Legacy-Clients) palīdz konsekventa uzvedība:

  • Retry-After: piemēram 1 līdz 3 sekundes, atkarībā no Endpoint. Tas ir skaidrs taktizētājs.
  • Kurzer Body: Mazs JSON, piemēram {"error":"server_busy","requestId":"..."}, pilnīgi pietiek. Lieli error-objekti atkal maksā CPU un joslas platumu.
  • Health-Endpoint ungedrosselt: Monitoringam arī slodzes laikā jāsniedz indikācijas (ja nepieciešams ar „degraded“-Flag).

Ja pirms lietojumprogrammas stāv Reverse Proxy kā nginx: saskaņojiet tur timeouts un buffering. Proxy var atvieglot slodzi (TLS-Termination, Keep-Alive), bet arī pārbīdīt to (piem., lielu Request-Body pufferēšana). Ekspluatācijā svarīgi, lai limiti būtu konsekventi: Proxy-Timeout > App-Timeout, citādi Clients redzēs „Gateway Timeout“, lai gan App būtu korekti noraidījusi pieprasījumu.

Threading, DB-Pools und Keep-Alive: Wo es in der Praxis kippt

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-Ablosung mit nativer Anbindung-Pooling und Transaktionen: BDE-Ablosung mit nativer Anbindung kann Connections poolen, 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“.

Performance-Hebel neben dem Gate: JSON, DB und I/O nicht vermischen

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 (aptuvena cēloņu analīze, neignorējot datu aizsardzību)

Tas dod signālu, vai limiti ir pārāk stingri (pārāk daudz 429) vai pārāk mīksti (augsts WaitedMs, pieaugošas latentces). Un jūs redzat, vai atsevišķi maršruti dominē. Par Windows- un Linux-Services ikdienā tas ir izšķiroši: bez telemetrijas veiktspējas problēma ātri kļūst par minēšanu starp tīklu, datubāzi, proxy un lietojumprogrammu.

Neparasti, bet ārkārtīgi noderīgi: „WaitedMs“ kā agrā brīdinājuma indikators

Daudzas komandas skatās tikai uz atbildes laiku un CPU. WaitedMs bieži ir labāks indikators, jo tas parāda, ka pieprasījumi jau pirms pašas darba izpildes gaida. Ja WaitedMs pieaug, kamēr CPU paliek mērena, ierobežotā resursa parasti nav CPU, bet gan kāds pulks (DB savienojumi), bloķējums biznesa loģikā vai ārējs downstream-serviss. Tas ietaupa laiku cēloņu analīzē, jo jūs meklējat mērķtiecīgāk virzienā „Pool/Lock/I/O” nevis „Compiler-Optimierung”.

Varianti: Pro-maršruta vārti, prioritātes un „Fast Lane”

Vārti visam ir vienkārši, bet ne vienmēr ideāli. Jēgpilnas variācijas:

  • Vārti pa maršruta grupu: „/reports” stingri, „/api/orders” mēreni, „/health” atvērts. Tādējādi jūs novēršat, ka dārgi atskaišu pieprasījumi izspiež pamatprocesus.
  • Fast Lane administrācijai/uzraudzībai: Atsevišķi vārti ar ierobežotu paralelitāti, lai operācijas būtu iespējamas arī slodzes laikā.
  • Budžetam balstīti limiti: Ja atbildes lielumi būtiski svārstās, papildus var palīdzēt baitu budžets (piem., maksimāli X MB vienlaikus ģenerēšanā). Tas ir sarežģītāk, bet pie lieliem lejupielāžu apjomiem reālistiski.

Svarīgi: Prioritizācija ātri kļūst politiska („mans Endpoint ir svarīgāks”). Tehniski stabils risinājums saglabājas, ja prioritātes ir sasaistītas ar procesiem (piem., pasūtījumu reģistrācija pirms atskaišu veidošanas), nevis ar lomām vai nodaļām.

Secinājums: Vai vārti atmaksājas — un kur pieejai rodas robežas?

Concurrency-Gate ir pragmatisks elements High Performance REST serverim Delphi, jo tas padara pārslodzi pārvaldāmu un saglabā jūsu sistēmas stabilas maksimālās slodzes laikā. Tas īpaši atmaksājas, ja jums ir datubāzei piesaistīti galapunkti, ja priekšā ir Reverse Proxy vai ja vairāki klienti (Legacy, Portale, Services) vilnī rada slodzi.

Robežas ir skaidras: ja faktiskais darbs uz pieprasījumu ir pārāk dārgs (neefektīvi vaicājumi, lieli JSON objekti, bloķējošas ārējās sistēmas), vārti tikai maskē simptomus. Tad jāuzlabo datu piekļuve, kešēšanas stratēģijas, timeouts un, ja nepieciešams, asinhronā apstrāde (Queue/Job-System). Tomēr ekspluatācijā kā drošības josta vārti bieži ir atšķirība starp „nedaudz lēns” un „pilnīgi neizmantojams”.

Ja vēlaties ieviest pārslodzes uzvedību esošā Delphi REST-API un REST-Server vai rūpīgi sabalansēt limitus ar datubāzes un proxy time‑outiem: apspriediet projektu vai modernizācijas ieceri ar Net-Base.

Profesionālajā kontekstā arī Thread-Pool Delphi un Http 429 Too Many Requests spēlē svarīgu lomu, ja integrācijas, datu plūsmas un turpmākā attīstība ir jāpanāk sakārtoti.

Apspriest projektu vai modernizācijas ieceri ar Net-Base.

Nākamais solis

Ja no tēmas rodas reāls projekts, arhitektūra, esošais stāvoklis un ekspluatācija būtu jāizskata kopā jau agri.

Mēs atbalstām ne tikai atsevišķu jautājumu risināšanā, bet arī tad, kad no avota koda fragmentiem, mantojuma sistēmu jautājumiem vai portāla idejām jāizveido stabils uzņēmuma līmeņa projekts.

  • Esošais stāvoklis, mērķa stāvoklis un tehniskie riski tiek kopīgi vērtēti.
  • REST, datu piekļuve, portāli un izvēršana netiek atlikti kā vēlākas sekas.
  • Jūs savlaicīgi redzat, kurš ceļš ir ekonomiski un darbības ziņā dzīvotspējīgs.

Kopīgot ierakstu

Kopīgot šo ierakstu tieši

LinkedIn, X, XING, Facebook, WhatsApp un e-pasts ir uzreiz pieejami. Instagramam saiti un īsu tekstu sagatavosim nekavējoties.

E-pasts

Instagram atveras jaunā cilnē. Saite un īss teksts tiek iepriekš nokopēti starpliktuvē.