Net-Base Revija

06.06.2026

Visoko zmogljiv REST strežnik v Delphi: omejitve zahtevkov, Thread-Pool in urejeno ravnanje pri preobremenitvi (izsek izvorne kode)

Visoko zmogljiv REST strežnik v Delphi ni hiter le zaradi 'hitrega JSON-a', temveč zaradi nadzorovane vzporednosti, strogih timeoutov in urejenega obnašanja pri preobremenitvi. Ta prispevek prikazuje praktično Concurrency-Gate z semaforjem, odgovori 429/503...

06.06.2026

Od teme v reviji do projektne prakse

Ustrezne strani storitev in tehnični opisi k prispevku

Zakaj „High Performance“ pri REST v Delphi pogosto odpove zaradi paralelizma

En strežnik z visokimi zmogljivostmi za REST Delphi v praksi redko naleti na omejitev zgolj zaradi CPU-časa na zahtevo, temveč zaradi nekontroliranega paralelizma: preveč sočasnih zahtev, preveč sočasnih podatkovno-baznih poizvedb ali blokirajoči I/O (datoteke, omrežje, baza). Rezultat ne deluje kot »malo počasneje«, temveč kot verižna reakcija: več niti, daljše vrste, zrušitev connection poola, naraščajoče zakasnitve, time-outi na strani odjemalca in na koncu strežnik, ki sicer še »živi«, vendar ne daje več zanesljivih odgovorov.

Protistrup ni en sam trik, ampak namerno obnašanje ob preobremenitvi: ko strežnik doseže svoje meje, mora zgodaj in deterministično zavrniti (tipično HTTP 429 ali 503), namesto da bi zahteve pustil tekati v neskončno vrsto. Za to je ta izvleček iz izvorne kode namenjen: lahka Concurrency-Gate (semafor) z timeouti, ki se vgradi v obstoječe REST-endpoint-e – ne glede na to, ali uporabljate Indy, WebBroker, Horse ali lastno HTTP-plast.

Arhitekturna ideja: Concurrency-Gate pred »zahtevnim delom«

Osnovna ideja je preprosta: pred zahtevnim delom (dostop do baze, kompleksna poročila, velike JSON-odgovore) se rezervira token iz semaforja. Če ni prostega tokena, se takoj pošlje kontroliran odgovor. Pomembno je: ta gate mora biti zanesljivo sproščen (try/finally) in mora biti nameščen v tisti del kode, ki je res zahtevna – ne zgolj na samem začetku Request-Handlerja, če se bo potem vseeno izvedel parser/router/overjanje.

S tem se obremenitev ne »odstrani«, temveč usmeri: strežnik hkrati odgovori na manj zahtev, a z bolj stabilnimi zakasnitvami. V individualnih poslovnih aplikacijah je to običajno bolj vredno kot občasni rekordi v sintetičnih benchmarkih.

Izvleček iz izvorne kode: omejevalnik zahtev z timeoutom, 429/503 in telemetričnimi hooki

Naslednja Delphi-koda implementira Concurrency-Gate kot razred TRestRequestGate. Temelji na TSemaphore (iz System.SyncObjs; semafor je števec za omejene sočasne dostope). Klic gate-a vrne bodisi »lease«-objekt (RAII-ähnlich: sprostitev v Destructorju) bodisi odločitve za takojšnji odgovor o preobremenitvi. Dodatno so na voljo hooki za logging/monitoring, da lahko v obratovanju vidite, zakaj so bile zahteve zavrnjene.

Delphi
unit RESTRequestGate;

interface

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

type
  // Minimalen kontekst za logiranje/sledenje; lahko se npr. razširi z User/Route.
  TRESTGateContext = record
    RequestId: string;
    Route: string;
    RemoteIp: string;
  end;

  TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);

  // Hook za telemetrijo obratovanja (npr. v datoteko, Syslog, Prometheus-eksporter, itd.)
  TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
                                         Decision: TRESTOverloadDecision;
                                         WaitedMs: Integer;
                                         InFlight: Integer);

  // Lease-objekt: sprostitev tokena v destruktorju.
  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: brez čakanja, takoj 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;

  // Najprej zmanjša števec, nato sprosti semafor.
  TInterlocked.Decrement(FInFlightCounter^);
  FSemaphore.Release;
end;

{ TRESTRequestGate }

constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
  inherited Create;
  if AMaxInFlight <= 0 then
    raise EArgumentException.Create('AMaxInFlight mora biti > 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: namensko čakanje, vendar omejeno.
        Decision := odRejectedTimeout;
        Result := False;
        if Assigned(FOnEvent) then
          FOnEvent(Ctx, Decision, WaitedMs, InFlight);
      end;
  else
    begin
      // wrAbandoned/napake: konservativno zavrniti
      Decision := odRejectedBusy;
      Result := False;
      if Assigned(FOnEvent) then
        FOnEvent(Ctx, Decision, WaitedMs, InFlight);
    end;
  end;
end;

end.

Namen: Stabilnost pod obremenitvijo namesto „vse hkrati“

Z MaxInFlight določite, koliko zahtev (Requests) sme hkrati v „dražji del“. Namenoma to ni „število CPU jeder“, temveč operativna velikost. Pri endpointih, kjer prevladuje obremenitev baze podatkov, je pogosto smiselno nastaviti MaxInFlight v sorazmerju s DB-Connection-Pool (na primer Pool = 20, MaxInFlight = 12 do 16), da vsak request ne zasede povezave in nato blokira nadaljnje niti.

Omejitve in pasti

  • Try/Finally ist Pflicht: Lease mora biti zagotovo sproščen. Če imate v endpointu izjeme, bo drugače Gate „puščal“ in strežnik bo trajno ostal v „busy“ načinu.
  • Timeout sinnvoll wählen: TimeoutMs=0 je trda meja (takoj zavrniti). Kratek timeout (tipično 50 do 150 ms) zgladi vrhove, ne da bi vzpostavil resnične vrste čakajočih.
  • Gate nicht zu früh: Avtentikacija (na primer Bearer/JWT) ali routing je lahko smiselna predloga; semafor naj se vključi tik pred res dražjim delom. Obratno: če postane avtentikacija draga (npr. proti zunanjemu identity-sistemu), jo je treba omejiti tudi.
  • 429 vs 503: HTTP 429 („Too Many Requests“) je primeren, kadar želite, da klienti namensko ponovno poizkušajo. 503 („Service Unavailable“) pa ustreza, kadar storitev začasno na splošno ni sposobna smiselno sprejemati zahtev. V obeh primerih je priporočljiv Retry-After-header.

Integration in REST-Handler: Indy/WebBroker/Horse pragmatisch

Snippet je namerno framework-nevtralen. Potrebujete le mesto, kjer zahteve „stekajo“ skozi. Tipično je to globalni singleton ali gate na skupino poti (na primer „/reports“ z manjšim gate, „/health“ brez gata). Primer vgradnje kot vzorec:

  • Izpolnite kontekst (RequestId, Route, RemoteIp)
  • TryAcquire s kratkim timeoutom
  • Ob zavrnitvi takoj zapisati odgovor (429/503) in končati
  • Lease ostane v scope do konca dražjega dela

V Horse (Middleware) je gate blizu skupine poti. V WebBrokerju lahko delate v posameznem Action-Handlerju. Pri Indy je odvisno, ali imate za vsako zahtevo posebej nit; gate vseeno deluje, dokler so dragi odseki jasno omejeni.

High Performance REST Server Delphi: Overload-Antworten, die Clients nicht „vergiften“

Odgovori ob preobremenitvi so več kot statusne kode. Če klienti ob 429/503 agresivno takoj ponovno pošljejo zahteve, se sproži val ponovnih poizkusov. V heterogenih sistemskih okoljih (mobilne aplikacije, C# Services, legacy-klienti) pomaga dosledno vedenje:

  • Retry-After: na primer 1 do 3 sekunde, odvisno od endpointa. To je jasen takt.
  • Kratek Body: Majhen JSON, na primer {"error":"server_busy","requestId":"..."}, zadostuje. Veliki error-objekti ponovno porabijo CPU in pasovno širino.
  • Health-Endpoint ungedrosselt: Monitoring mora tudi pri obremenitvi še vedno dajati informacije (po potrebi z „degraded“ zastavico).

Če pred aplikacijo teče reverzni proxy, kot je nginx: uskladite timeoute in buffering tam. Proxy lahko razbremeni (TLS-terminacija, Keep-Alive), lahko pa tudi premakne obremenitev (na primer predpomnjenje velikih request-bodyjev). V obratovanju šteje, da so omejitve konsistentne: Proxy-Timeout > App-Timeout, sicer klienti vidijo „Gateway Timeout“, čeprav bi aplikacija zahtevo korektno zavrnila.

Threading, DB-puli und Keep-Alive: kje v praksi pride do zasičenja

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:

  • En zahtevek blokira več omejenih virov: Najprej ena DB-povezava, potem zunanji HTTP-klic, nato dostop do datoteke. Če se vse to zgodi v isti niti zahtevka, se čas blokade množi. Gate sicer omeji paralelizem, vendar prepustnost drastično pade. Tu se izplača razvezati odvisnosti (npr. zunanje klice izvesti asinhrono, predpripravo izvajati prek vrste opravil / Job-Queue).
  • BDE-nadomestitev z nativno vezavo-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 kot skriti porabnik pomnilnika: Keep-Alive zmanjšuje handshakе, lahko pa pri številnih neaktivnih klientih povzroči preveč odprtih socketov. Ravno pri Windows- in Linux-storitvah se ne vidi »povišane CPU«, temveč »polni Handles/FDs« ali povečana raba RAM zaradi bufferjev. Tu pomagajo jasni idle-timeouti na strežniku in na reverse proxyju ter omejitev na klient-IP, če to okolje dopušča.

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:

  • Sestavljanje JSON-a z nepotrebnimi vmesnimi nizi: Pogosto se obremenitev pojavi zaradi številnih začasnih Unicode-nizov. Kjer je mogoče, gradite streaming usmerjeno (Writer/Stream) namesto velikih vmesnih objektov, še posebej pri endpointih, ki vračajo sezname.
  • Dostop do baze „po elementu“: N+1-Queries in per-Row-Lookups so klasika. Bolje: ciljni joins, batch-queries, agregacija na strežniku. Pri zelo velikih rezultatih se izplača tudi paginacija s stabilnim razvrščanjem (tako strani ne bodo «skakale»).
  • Blokirajoči I/O v niti zahtevka: Dostopi do datotek ali zunanji HTTP-klici naj bodo strogo omejeni ali premaknjeni v asinhrono cevovodje. Sicer zasedete drage niti z »čakanjem«.

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 (groba analiza vzrokov, brez zanemarjanja varstva podatkov)
  • S tem dobite signal, ali so omejitve preveč stroge (preveč 429) ali preveč ohlapne (visoki WaitedMs, naraščajoče latence). In vidite, ali posamezne poti dominirajo. Za Windows- und Linux-Services je to v vsakdanjem delu odločilno: brez telemetrije se težava s performansom hitro spremeni v ugibanje med omrežjem, podatkovno bazo, proxyjem in aplikacijo.

    Ungewöhnlich, aber extrem hilfreich: „WaitedMs“ als Frühwarnindikator

    Veliko ekip gleda le odzivni čas in CPU. WaitedMs je pogosto boljši indikator, ker pokaže, da zahtevki že pred dejanskim delom čakajo. Če WaitedMs narašča, medtem ko CPU ostaja zmeren, pogosto ni omejena sredstva CPU, temveč pool (DB-Verbindungen), zaklep v poslovni logiki ali zunanji downstream-service. To prihrani čas pri analizi vzrokov, saj ciljno iščete v smeri „Pool/Lock/I/O“ namesto „Compiler-Optimierung“.

    Varianten: Pro-Route-Gates, Prioritäten und „Fast Lane“

    Ena kapija za vse je preprosta, vendar ne vedno idealna. Smiselne variante:

    • Gate za skupino poti: „/reports“ strogo, „/api/orders“ zmerno, „/health“ odprto. Tako preprečite, da dragi zahtevki za poročila izrinjajo jedrne procese.
    • Fast Lane za Admin/Monitoring: ločen Gate z majhno paralelnostjo, da so upravljalska dejanja možna tudi pod obremenitvijo.
    • Omejitve na osnovi proračuna: Če se velikosti odziva močno razlikujejo, lahko dodatno pomaga byte-proračun (npr. maksimalno X MB hkrati pri generiranju). To je bolj kompleksno, vendar realno pri velikih prenosih.

    Pomembno: prioritizacija hitro postane politična („moj Endpoint je pomembnejši“). Tehnično stabilno ostane, če so prioriteti vezane na procese (npr. Auftragserfassung pred Reporting), ne na vloge ali oddelke.

    Fazit: Lohnt sich das Gate – und wo kippt der Ansatz?

    Concurrency-Gate je pragmatičen gradnik za visokozmogljiv REST strežnik v Delphi, saj naredi overload obvladljiv in vaše sisteme ohranja stabilne pri vršnem bremenu. Še posebej se izplača, če imate podatkovno-bazno vezane endpoints, če stoji pred njim Reverse Proxy ali če več klientov (Legacy, Portale, Services) v valovih ustvarja obremenitev.

    Meje so jasne: če je dejansko delo na zahtevo predrago (ineffiziente Queries, veliki JSON-Objekte, blockierende Fremdsysteme), maskira Gate le simptome. Potem je treba izboljšati dostop do podatkov, strategije predpomnenja, Timeouts in po potrebi asinhrono obdelavo (Queue/Job-System). Kot varnostni pas v obratovanju pa je Gate pogosto razlika med „le začasno počasno“ in „popolnoma neuporabno“.

    Če želite vpeljati Overload-Verhalten v obstoječo Delphi REST-API und REST-Server ali natančno uravnotežiti omejitve z Datenbank- und Proxy-Timeouts: Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

    V strokovnem kontekstu igrajo pomembno vlogo tudi Thread-Pool Delphi und Http 429 Too Many Requests, kadar morajo Integrationen, Datenflüsse und Weiterentwicklung dobro sodelovati.

    Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

    Naslednji korak

    Ko se tema spremeni v dejanski projekt, je treba arhitekturo, obstoječi sistem in obratovanje zgodaj obravnavati skupaj.

    Ne podpiramo le pri posameznih vprašanjih, ampak tudi takrat, ko iz izrezkov izvorne kode, legacy-tem ali idej za portale nastane zanesljiv podjetniški projekt.

    • Obstoječe stanje, ciljno stanje in tehnična tveganja se ocenjujejo skupaj.
    • REST, dostop do podatkov, portali in uvedba niso prestavljeni kot poznejše posledice.
    • Zgodaj prepoznate, katera pot je ekonomsko in obratovalno vzdržna.

    Deli objavo

    Deli ta prispevek neposredno

    LinkedIn, X, XING, Facebook, WhatsApp in e-pošta so takoj na voljo. Za Instagram bomo neposredno pripravili povezavo in kratek opis.

    E-pošta

    Instagram se odpre v novem zavihku. Povezava in kratek opis se pred tem kopirata v odložišče.