Net-Base Revista

06.06.2026

Servidor d'alt rendiment REST en Delphi: límits de peticions, pool de fils i maneig net de sobrecàrrega (fragments de codi font)

Un servidor REST d'alt rendiment en Delphi no és ràpid només per un 'JSON ràpid', sinó per paral·lelisme controlat, timeouts estrictes i un comportament net davant la sobrecàrrega. Aquest article mostra un Concurrency-Gate pràctic amb semàfor, respostes 429/503...

06.06.2026

Del tema de la revista a la pràctica del projecte

Pàgines de serveis i tècniques pertinents per a l'article

Per què «d’alt rendiment» en REST a Delphi sovint fracassa per la paral·lelitat

Un servidor d’alt rendiment REST Delphi rarament està limitat en la pràctica només per temps de CPU per petició, sinó per paral·lelitat descontrolada: massa peticions simultànies, massa consultes a la base de dades simultànies o E/S bloquejant (fitxer, xarxa, base de dades). El resultat no es percep com «una mica més lent», sinó com una reacció en cadena: més threads, més cues d’espera, col·lapse del pool de connexions, latències creixents, timeouts al client i, al final, un servidor que encara «està viu» però ja no proporciona respostes estables.

La contramesura no és un truc isolat, sinó un comportament de sobrecàrrega conscient: quan el servidor arriba als seus límits, ha de rebutjar de manera precoç i determinista (típicament HTTP 429 o 503), en lloc d’encolar les peticions en una cua infinita. Precisament per això serveix aquest fragment de codi: un Concurrency-Gate lleuger (Semaphore) més timeouts, que es pot integrar en els endpoints de REST existents – independentment de si utilitzeu Indy, WebBroker, Horse o una capa HTTP pròpia.

Idea d’arquitectura: Concurrency-Gate abans de la «part costosa»

La idea bàsica és simple: abans de la part costosa (accés a la base de dades, informes complexos, respostes JSON grans) es reserva un token d’una semaphore. Si no hi ha cap token lliure, s’envia immediatament una resposta controlada. És important: aquest gate ha de ser alliberat de manera fiable (try/finally), i ha d’estar en el camí del codi que realment és costós — no només al principi del request handler quan després encara vindran parser/router/autenticació.

D’aquesta manera la càrrega no s’«elimina», sinó que es canalitza: el servidor respon menys peticions simultàniament, però amb latències més estables. En aplicacions empresarials a mida això normalment és més valuós que temps de rècord esporàdics en benchmarks sintètics.

Fragment de codi: limitador de peticions amb Timeout, 429/503 i hooks de telemetria

El següent codi Delphi implementa un Concurrency-Gate com a classe TRestRequestGate. Es basa en TSemaphore (de System.SyncObjs; una semàfora és un comptador per a accessos simultanis limitats). La crida al gate retorna o bé un objecte «Lease» (semblant a RAII: alliberament al destructor) o opta per una resposta immediata de sobrecàrrega. A més, s’hi inclouen hooks per a logging/monitoring, perquè pugueu veure en explotació per què s’han rebutjat les peticions.

Delphi
unit RESTRequestGate;

interface

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

type
  // Context mínim per a registres i tracing; es pot ampliar, per exemple, amb usuari o ruta.
  TRESTGateContext = record
    RequestId: string;
    Route: string;
    RemoteIp: string;
  end;

  TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);

  // Hook per a la telemetria d'operació (p. ex. a fitxer, Syslog, Prometheus-Exporter, etc.)
  TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
                                         Decision: TRESTOverloadDecision;
                                         WaitedMs: Integer;
                                         InFlight: Integer);

  // Objecte Lease: alliberament del token al 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: cap temps d'espera, immediatament 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;

  // Primer decrementar el comptador, després alliberar el semàfor.
  TInterlocked.Decrement(FInFlightCounter^);
  FSemaphore.Release;
end;

{ TRESTRequestGate }

constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
  inherited Create;
  if AMaxInFlight <= 0 then
    raise EArgumentException.Create('AMaxInFlight ha de ser > 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 quan TimeoutMs > 0: espera dirigida però limitada.
        Decision := odRejectedTimeout;
        Result := False;
        if Assigned(FOnEvent) then
          FOnEvent(Ctx, Decision, WaitedMs, InFlight);
      end;
  else
    begin
      // wrAbandoned/casos d'error: rebutjar de manera conservadora
      Decision := odRejectedBusy;
      Result := False;
      if Assigned(FOnEvent) then
        FOnEvent(Ctx, Decision, WaitedMs, InFlight);
    end;
  end;
end;

end.

Objectiu: estabilitat sota càrrega en lloc de „tot al mateix temps“

Amb MaxInFlight definiu quants Requests poden entrar simultàniament a la „part cara“. Això deliberadament no és el „nombre de nuclis de CPU“, sinó una magnitud d’operació. Per a endpoints amb molta càrrega de base de dades sovint té sentit ajustar MaxInFlight en relació amb el DB-Connection-Pool (per exemple Pool = 20, MaxInFlight = 12 a 16), perquè no cada Request bloquegi una connexió i després facin falta més Threads.

Condicions marc i punts crítics

  • Try/Finally és obligatori: El Lease s’ha de garantir que es alliberi. Si teniu Exceptions a l’Endpoint, el gate deixarà de funcionar correctament i el servidor romandrà de manera permanent en „busy“.
  • Escollir Timeout de manera raonable: TimeoutMs=0 és un límit rígid (rebutjar immediatament). Un timeout curt (típicament 50 a 150 ms) suavitza els pics sense crear veritables cues d’espera.
  • No col·locar el gate massa aviat: L’autenticació (per exemple Bearer/JWT) o el routing poden ser adequats; la semàfora hauria d’actuar abans de la secció realment cara. A l’inrevés: si l’autenticació és cara (p. ex. contra un sistema d’identitat extern), també cal limitar-la.
  • 429 vs 503: HTTP 429 („Too Many Requests“) és adequat quan es vol que els clients reintentin de manera selectiva. 503 („Service Unavailable“) s’ajusta quan el servei temporalment, de manera general, no està en condicions d’acceptar peticions de forma útil. En ambdós casos és recomanable un Retry-After-Header.

Integració en REST-Handler: Indy/WebBroker/Horse pragmàtic

El snippet és deliberadament framework-neutral. Només necessiteu un lloc on les Requests „passin“. Típic és un singleton global o un gate per grup de rutes (per exemple „/reports“ més petit, „/health“ sense gate). Exemples d’integració com a model:

  • Omplir el context (RequestId, Route, RemoteIp)
  • TryAcquire amb un timeout curt
  • En cas de denegació escriure la resposta immediatament (429/503) i acabar
  • El Lease viu dins l’abast fins després de la part cara

A Horse (Middleware) el gate queda a prop d’un grup de rutes. En WebBroker podeu treballar dins de l’Action-Handler corresponent. En Indy depèn de si teniu un Thread per Request; el gate actua igualment, sempre que les parts cares estiguin clarament limitades.

Servidors High Performance REST Delphi: respostes d’excés de càrrega que no „enverinin“ els clients

Les respostes d’una sobrecàrrega són més que codis d’estat. Si els clients, davant de 429/503, tornen a enviar agressivament de seguida, es produeix una tempesta de reintents. En paisatges de sistemes heterogenis (Mobile Apps, C# Services, Legacy-Clients) ajuda un comportament consistent:

  • Retry-After: per exemple 1 a 3 segons, segons l’Endpoint. Això és un compàs clar.
  • Cos curt: Un petit JSON com {"error":"server_busy","requestId":"..."} és suficient. Objectes d’error voluminosos tornen a consumir CPU i ample de banda.
  • Health-Endpoint sense limitacions: El monitoring ha de poder donar informació encara sota càrrega (si cal, amb un flag „degraded“).

Si executeu un Reverse Proxy com nginx davant: cal ajustar Timeouts i Buffering allà. Un Proxy pot alleujar (TLS-Termination, Keep-Alive), però també desplaçar la càrrega (p.ex. amortir Request-Bodies grans). En producció importa que els límits siguin consistents: Proxy-Timeout > App-Timeout, sinó els clients veuran „Gateway Timeout“ malgrat que l’App hagués rebutjat correctament.

Threading, DB-Pools i Keep-Alive: on es descompensa a la pràctica

El Gate resol el problema de „massa al mateix temps“, però no impedeix automàticament que una sola request vinculi excessivament recursos. Tres punts típics de descompensació en projectes Delphi apareixen just a les interfícies entre threading, base de dades i connexions HTTP:

  • Una request bloqueja diverses recursos escassos: Primer una connexió DB, després una crida HTTP externa, després un accés a fitxer. Si tot això passa en el mateix thread de la request, els temps de bloqueig se multipliquen. El Gate limitarà la paral·lelitat, però el rendiment global cau dràsticament. Val la pena desacoblar aquestes dependències (p. ex. crides externes asíncrones, precàlcul mitjançant Job-Queue).
  • BDE-Ablosung mit nativer Anbindung-Pooling i transaccions: BDE-Ablosung mit nativer Anbindung pot fer pooling de connexions, però una transacció „llarga“ (p. ex. perquè la creació de JSON o les comprovacions de negoci es fan entre StartTransaction i Commit) manté la connexió innecesàriament. Una pràctica correcta és mantenir la transacció tan ajustada com sigui possible al voltant dels statements reals i serialitzar o validar fora de la transacció quan sigui possible des del punt de vista funcional.
  • HTTP Keep-Alive com a consumidor de memòria ocult: Keep-Alive redueix els handshakes, però amb molts clients en repòs pot generar massa sockets oberts. Especialment en Windows- i Linux-Services no es veu un „CPU alta“, sinó „Handles/FDs plens“ o consum de RAM per búfers. Aquí ajuden timeouts d’idle clars al servidor i al reverse proxy, així com un límit per Client-IP si l’entorn ho permet.

La conseqüència: MaxInFlight no és un valor estàtic. Depèn del recurs més lent i més escàs del vostre entorn (DB, sistemes externs, Storage) i de com de bé una request „manté“ aquests recursos.

Palanques de rendiment més enllà del Gate: no barrejar JSON, DB i I/O

El Gate estabilitza, però no substitueix una economia d’endpoints ben definida. Tres frens que reapareixen sovint en servidors Delphi REST són:

  • Construcció de JSON amb cadenes intermèdies innecessàries: Sovint la càrrega prové de moltes cadenes Unicode temporals. Allà on sigui possible, construir orientat a l’streaming (Writer/Stream) en lloc de crear grans objectes intermedis, especialment en endpoints de llistes.
  • Accés a base de dades „per item“: Les N+1-queries i les consultes per fila són el clàssic. Millor: joins dirigits, batch-queries, agregació al costat del servidor. Per resultats molt grans convé, a més, paginació amb ordenació estable (per evitar que les pàgines „saltiïn“).
  • I/O bloquejant en el thread de la request: Els accessos a fitxers o les crides HTTP externes s’han d’estrènyer o traslladar a una pipeline asíncrona. Altrament, es bloquejaran threads costosos simplement per „esperar“.

Per a solucions digitals empresarials evolucionades aquest sovint és el punt crític: un endpoint s’ha afegit „a corre-cuita“ i funciona fins que arriben càrregues reals i grans volums de dades. Llavors queda clar si s’han traçat límits d’arquitectura nets (capa d’accés a dades, caching, estratègies bulk, timeouts clars).

Depuració i explotació: què heu de mesurar

El Hook OnEvent és deliberadament senzill. A la pràctica caldria capturar com a mínim els següents valors:

  • InFlight (paral·lelisme actual al Gate)
  • WaitedMs (quant de „Queueing“ permeteu)
  • Decision (accepted/busy/timeout)
  • Route/RemoteIp (anàlisi de causes a grans trets, sense ignorar la protecció de dades)

Això li dona un senyal per saber si els límits són massa estrictes (massa 429) o massa permissius (WaitedMs elevat, latències en augment). I permet veure si rutes concretes dominen. Per a Windows- i Linux-Services això és decisiu en el dia a dia: sense telemetria un problema de rendiment ràpidament es converteix en un joc d’endevinalles entre xarxa, base de dades, proxy i aplicació.

Insòlit, però extremadament útil: „WaitedMs“ com a indicador d’alerta primerenca

Molts equips només es fixen en el temps de resposta i la CPU. WaitedMs sovint és el millor indicador, perquè mostra que les peticions ja estan esperant abans de la feina real. Si WaitedMs puja mentre la CPU es manté moderada, el recurs limitat sovint no és la CPU, sinó un pool (connexions a la base de dades), un bloqueig en la lògica de negoci o un servei downstream extern. Això estalvia temps en l’anàlisi de causes, perquè podreu buscar més dirigits cap a «Pool/Lock/I/O» en comptes de «optimització del compilador».

Variants: Gates per ruta, prioritats i „Fast Lane“

Un Gate per a tot és senzill, però no sempre ideal. Variants recomanables:

  • Gate per grup de rutes: „/reports“ estricte, „/api/orders“ moderat, „/health“ obert. Així eviteu que peticions de reports costoses desplacin processos crítics.
  • Fast Lane per a administració/monitoratge: Gate separat amb poca paral·lelitat, perquè les accions d’operació siguin possibles també sota càrrega.
  • Límits basats en pressupost: Quan les mides de resposta varien molt, un pressupost de bytes addicional pot ajudar (p.ex. màxim X MB simultanis en la generació). Això és més complex, però realista per a descàrregues grans.

Important: la priorització es fa ràpidament política («el meu endpoint és més important»). Tècnicament es manté estable si les prioritats s’enllacen a processos (p.ex. entrada de comandes abans d’informes), no a rols o departaments.

Conclusió: Convé el Gate — i on falla l’enfocament?

Un Concurrency-Gate és un element pragmàtic per a un servidor High Performance REST en Delphi, perquè fa que l’overload sigui controlable i manté els vostres sistemes estables durant pics de càrrega. Convé especialment si teniu endpoints lligats a base de dades, si hi ha un reverse proxy davant o si diversos clients (Legacy, portals, Services) generen càrrega en ones.

Els límits són clars: si la feina per request és massa costosa (consultes ineficients, grans objectes JSON, sistemes externs que bloquegen), el Gate només amaga símptomes. Cal millorar l’accés a dades, les estratègies de caching, els timeouts i, si escau, el processament asíncron (sistema de cues/feines). Com a cinturó de seguretat en l’explotació, el Gate sovint és la diferència entre «temporalment degradat» i «completament inservible».

Si voleu introduir comportament d’overload en una Delphi REST-API und REST-Server existent o equilibrar límits amb timeouts de base de dades i proxy de manera neta: parleu del projecte o del pla de modernització amb Net-Base.

En l’àmbit tècnic, també juguen un paper important el Thread-Pool Delphi i Http 429 Too Many Requests, quan integracions, fluxos de dades i evolució han de funcionar conjuntament de manera ordenada.

Parleu del projecte o del pla de modernització amb Net-Base.

Pas següent

Quan un tema esdevé un projecte real, l'arquitectura, l'entorn existent i les operacions s'haurien de considerar conjuntament des de bon començament.

No només donem suport en qüestions puntuals, sinó també quan, a partir de fragments de codi font, temes de sistemes heredats o idees de portal, ha de sorgir un projecte empresarial sòlid.

  • L'estat actual, la visió objectiu i els riscos tècnics s'avaluen conjuntament.
  • REST, l'accés a les dades, els portals i el desplegament no es releguen a fases posteriors.
  • Vostè veurà aviat quin camí és econòmicament i operativament viable.

Comparteix la publicació

Comparteix aquesta publicació directament

LinkedIn, X, XING, Facebook, WhatsApp i E-Mail estan disponibles de forma immediata. Per a Instagram preparem directament l’enllaç i un text breu.

Correu electrònic

Instagram s'obre en una pestanya nova. L'enllaç i el text curt es copien prèviament al porta-retalls.