Net-Base Revistă

06.06.2026

Server de înaltă performanță REST în Delphi: limite pentru cereri, thread-pool și comportament controlat la suprasarcină (fragment de cod sursă)

Un server de înaltă performanță REST în Delphi nu este rapid doar datorită „JSON-ului rapid“, ci prin concurență controlată, timeout-uri stricte și comportament curat la supraîncărcare. Acest articol prezintă un Concurrency-Gate aplicabil în practică cu semafor, răspunsuri 429/503...

06.06.2026

De la tema din revistă la practica în proiecte

Pagini relevante de servicii și pagini tehnice pentru articol

De ce „performanță ridicată” la REST în Delphi eșuează adesea din cauza paralelismului

Un server REST cu performanță ridicată Delphi este în practică rar limitat de timp CPU pur per cerere, ci de paralelism necontrolat: prea multe cereri simultane, prea multe interogări de bază de date simultane sau I/O blocant (fișier, rețea, bază de date). Rezultatul nu se simte ca „puțin mai lent”, ci ca o reacție în lanț: mai multe thread-uri, mai multe cozi de așteptare, colapsul pool-ului de conexiuni, latențe în creștere, time-out-uri la nivelul clientului și, în cele din urmă, un server care încă „trăiește”, dar nu mai oferă răspunsuri stabile.

Contramăsura nu e un truc izolat, ci un comportament de gestionare a supraîncărcării: când serverul ajunge la limite, trebuie să respingă devreme și determinist (tipic HTTP 429 sau 503), în loc să lase cererile să intre într-o coadă nesfârșită. Exact pentru asta este gândit acest fragment de cod sursă: un Concurrency-Gate ușor (Semaphore) plus timeout-uri, care se poate integra în endpoint-urile existente REST – indiferent dacă folosiți Indy, WebBroker, Horse sau un strat HTTP propriu.

Idee de arhitectură: Concurrency-Gate înainte de „partea costisitoare”

Ideea de bază este simplă: Înainte de partea costisitoare (acces la baza de date, rapoarte complexe, răspunsuri JSON mari) se rezervă un token dintr-o semaforă. Dacă niciun token nu este liber, se returnează imediat un răspuns controlat. Important: acest gate trebuie eliberat fiabil (try/finally), și trebuie plasat în calea de cod care este cu adevărat costisitoare – nu doar la începutul handler-ului de cereri, când urmează apoi parser/router/autentificare.

Astfel, încărcarea nu este „optimizată” pentru a dispărea, ci canalizată: serverul răspunde la mai puține cereri simultan, dar cu latențe mai stabile. În aplicațiile enterprise personalizate, asta este de obicei mai valoroasă decât timpi izolați buni în benchmark-uri sintetice.

Fragment de cod: limitator de cereri cu timeout, 429/503 și hook-uri de telemetrie

Codul următor pentru Delphi implementează un Concurrency-Gate ca și clasă TRestRequestGate. Se bazează pe TSemaphore (din System.SyncObjs; o semaforă este un contor pentru acces simultan limitat). Apelul gate-ului returnează fie un obiect „Lease” (asemănător RAII: eliberare în destructor), fie optează pentru un răspuns imediat de supraîncărcare. În plus există hook-uri pentru logging/monitorizare, astfel încât să vedeți în producție de ce cererile au fost respinse.

Delphi
unit RESTRequestGate;

interface

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

type
  // Context minim pentru logging/tracing; poate fi extins, de exemplu, cu User/Route.
  TRESTGateContext = record
    RequestId: string;
    Route: string;
    RemoteIp: string;
  end;

  TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);

  // Hook pentru telemetria de operare (de exemplu în fișier, Syslog, Prometheus-Exporter etc.)
  TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
                                         Decision: TRESTOverloadDecision;
                                         WaitedMs: Integer;
                                         InFlight: Integer);

  // Obiect Lease: eliberarea token-ului în destructor.
  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: fără timp de așteptare, imediat 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;

  // Mai întâi decrementăm contorul, apoi eliberăm semaforul.
  TInterlocked.Decrement(FInFlightCounter^);
  FSemaphore.Release;
end;

{ TRESTRequestGate }

constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
  inherited Create;
  if AMaxInFlight <= 0 then
    raise EArgumentException.Create('AMaxInFlight trebuie să fie > 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 când TimeoutMs > 0: așteptare intenționată, dar limitată.
        Decision := odRejectedTimeout;
        Result := False;
        if Assigned(FOnEvent) then
          FOnEvent(Ctx, Decision, WaitedMs, InFlight);
      end;
  else
    begin
      // wrAbandoned/erori: respingere conservatoare
      Decision := odRejectedBusy;
      Result := False;
      if Assigned(FOnEvent) then
        FOnEvent(Ctx, Decision, WaitedMs, InFlight);
    end;
  end;
end;

end.

Scop: stabilitate sub sarcină în loc de „totul simultan”

Cu MaxInFlight definiți câte request-uri pot intra simultan în „partea costisitoare”. Acesta nu este în mod intenționat „numărul de nuclee CPU”, ci o mărime operațională. Pentru endpoint-uri cu sarcină mare pe baza de date este adesea recomandat să setați MaxInFlight în relație cu DB-Connection-Pool (de exemplu Pool = 20, MaxInFlight = 12 până la 16), astfel încât nu fiecare request să blocheze o conexiune și apoi să fie necesare thread-uri suplimentare.

Condiții și capcane

  • Try/Finally este obligatoriu: Lease-ul trebuie eliberat garantat. Dacă aveți excepții în endpoint, altfel Gate-ul devine „permeabil” și serverul rămâne permanent „ocupat”.
  • Selectați inteligent Timeout-ul: TimeoutMs=0 este un prag dur (respinge imediat). Un timeout scurt (tipic 50 până la 150 ms) netezește vârfurile fără a construi cozi reale.
  • Nu plasați Gate-ul prea devreme: Autentificarea (de exemplu Bearer/JWT) sau rutarea pot fi ieftine; semaforul ar trebui să intervină înaintea secțiunii cu adevărat costisitoare. Invers: dacă autentificarea devine costisitoare (de ex. împotriva unui sistem extern de identitate), și aceasta trebuie limitată.
  • 429 vs 503: HTTP 429 („Too Many Requests”) e potrivit când clienții trebuie să reîncerce deliberat. 503 („Service Unavailable”) e potrivit când serviciul, temporar, nu este în măsură să preia cereri în mod util. În ambele cazuri, un header Retry-After este recomandat.

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

Snippet-ul este intenționat neutru față de framework. Aveți nevoie doar de un loc în care request-urile „trec prin”. Tipic este un singleton global sau un Gate per grup de rute (de exemplu „/reports” mai mic, „/health” fără Gate). Mai jos integrarea ca model:

  • Completați contextul (RequestId, Route, RemoteIp)
  • TryAcquire cu timeout scurt
  • La respingere, scrieți imediat răspunsul (429/503) și terminați
  • Lease-ul rămâne în scope până după partea costisitoare

În Horse (middleware) Gate-ul se află aproape de un grup de rute. În WebBroker puteți lucra în handler-ul de acțiune corespunzător. În Indy depinde dacă aveți câte un thread per request; Gate-ul funcționează în continuare, atâta timp cât secțiunile costisitoare sunt clar delimitate.

High Performance REST Server Delphi: răspunsuri la supraîncărcare care nu „otrăvesc” clienții

Răspunsurile la supraîncărcare sunt mai mult decât coduri de status. Dacă clienții, la 429/503, trimit agresiv imediat din nou, veți obține un val de reîncercări. În peisaje de sisteme eterogene (Mobile Apps, C# Services, clienți legacy) un comportament consecvent ajută:

  • Retry-After: de exemplu 1 până la 3 secunde, în funcție de endpoint. Este un ritm clar.
  • Body scurt: Un JSON mic precum {"error":"server_busy","requestId":"..."} este suficient. Obiectele mari de eroare costă din nou CPU și lățime de bandă.
  • Health-endpoint ne-limitat: Monitorizarea trebuie să mai poată oferi informații chiar și sub sarcină (eventual cu un flag „degraded”).

Dacă rulați în față un reverse proxy precum nginx: potriviți timeouts și buffering-ul acolo. Un proxy poate degreva (TLS-termination, Keep-Alive), dar poate și muta sarcina (de exemplu tamponând request-body-uri mari). În exploatare contează ca limitele să fie consistente: Proxy-Timeout > App-Timeout, altfel clienții vor vedea „Gateway Timeout”, deși aplicația ar fi respins curat.

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

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. 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-înlocuire cu integrare nativă-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

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 (analiză preliminară a cauzelor, fără a ignora protecția datelor)

Astfel primiți un semnal dacă limitele sunt prea stricte (prea multe 429) sau prea permisive (WaitedMs ridicat, latențe în creștere). Și vedeți dacă anumite rute domină. Pentru Windows- und Linux-Services este acest lucru esențial în practică: fără telemetrie, o problemă de performanță devine rapid un joc de ghicit între rețea, bază de date, proxy și aplicație.

Neobișnuit, dar extrem de util: „WaitedMs“ ca indicator de avertizare timpurie

Multe echipe se uită doar la Response-Time și CPU. WaitedMs este adesea un indicator mai bun, deoarece arată că requests stau deja în așteptare înainte de munca propriu-zisă. Dacă WaitedMs crește în timp ce CPU rămâne moderat, resursa limitativă nu este de cele mai multe ori CPU, ci un pool (conexiuni DB), un lock în logica de business sau un serviciu downstream extern. Aceasta economisește timp la analiza cauzelor, pentru că veți căuta mai țintit în direcția „Pool/Lock/I/O” în loc de „optimizare a compilatorului”.

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

Un gate pentru toate este simplu, dar nu întotdeauna ideal. Variante utile:

  • Gate pe grupă de rute: „/reports“ strict, „/api/orders“ moderat, „/health“ deschis. Astfel evitați ca request-urile costisitoare de generare a rapoartelor să sufoce procesele esențiale.
  • Fast Lane pentru Admin/Monitoring: Un gate separat cu paralelism redus, astfel încât operațiunile de exploatare să fie posibile chiar și sub încărcare.
  • Limite bazate pe buget: Dacă dimensiunile răspunsurilor variază puternic, un buget în bytes poate ajuta suplimentar (de ex. maxim X MB simultan la generare). Este mai complex, dar realist pentru descărcări mari.

Important: prioritizarea devine repede politică („endpoint‑ul meu este mai important“). Din punct de vedere tehnic rămâne stabilă dacă prioritățile sunt cuplate la procese (de ex. înregistrarea comenzilor înaintea raportării), nu la roluri sau departamente.

Concluzie: Merită implementarea unui Gate — și unde cedează abordarea?

Un Concurrency-Gate este o componentă pragmatică pentru un High Performance REST Server în Delphi, deoarece face suprasolicitarea controlabilă și menține stabilitatea sistemelor dumneavoastră la vârf de încărcare. Este deosebit de util dacă aveți endpointuri legate de bază de date, dacă un Reverse Proxy stă înainte sau dacă mai mulți clienți (Legacy, portale, Services) generează sarcină în valuri.

Limitările sunt clare: dacă munca efectivă per request este prea costisitoare (interogări ineficiente, obiecte JSON mari, sisteme externe care blochează), Gate‑ul mască doar simptomele. Atunci trebuie corectate accesul la date, strategiile de caching, timeouts și, după caz, prelucrarea asincronă (Queue/Job-System). Ca centură de siguranță în exploatare, Gate‑ul este însă adesea diferența între „ușor lent” și „complet inutilizabil”.

Dacă doriți să introduceți comportamentul de overload într-un existent Delphi REST-API und REST-Server sau să echilibrați curat limitele cu timeouts de bază de date și proxy: discutați proiectul sau planul de modernizare cu Net-Base.

În contextul profesional, și Thread-Pool Delphi și Http 429 Too Many Requests joacă un rol important, când integrările, fluxurile de date și dezvoltarea continuă trebuie să funcționeze armonios.

Discutați proiectul sau planul de modernizare cu Net-Base.

Următorul pas

Când o temă devine un proiect real, arhitectura, infrastructura existentă și operarea trebuie analizate împreună de la început.

Nu oferim sprijin doar pentru întrebări punctuale, ci și atunci când fragmente de cod sursă, probleme legacy sau idei de portal trebuie transformate într-un proiect robust la nivel de companie.

  • Situația curentă, starea țintă și riscurile tehnice sunt evaluate împreună.
  • REST, accesul la date, portalurile și Rollout nu sunt amânate ca consecințe ulterioare.
  • Veți vedea din timp ce cale este viabilă din punct de vedere economic și operațional.

Partajează postarea

Distribuiți această postare direct

LinkedIn, X, XING, Facebook, WhatsApp și e-mail sunt disponibile imediat. Pentru Instagram pregătim linkul și textul scurt imediat.

E-mail

Instagram se deschide într-o filă nouă. Linkul și textul scurt se copiază în prealabil în clipboard.