Net-Base Lehti

06.06.2026

Korkean suorituskyvyn REST-palvelin kohteessa Delphi: pyyntörajoitukset, säiepooli ja hallittu ylikuormituskäyttäytyminen (lähdekoodikatkelma)

Korkean suorituskyvyn REST-palvelin ympäristössä Delphi ei nopeudu pelkästään "nopean JSONin" ansiosta, vaan kontrolloidun samanaikaisuuden, tiukkojen aikakatkaisujen ja siistin ylikuormituskäytöksen kautta. Tämä kirjoitus esittelee käytännöllisen Concurrency-Gaten, joka perustuu semaforiin ja palauttaa 429/503-vastauksia...

06.06.2026

Lehden aiheesta projektikäytäntöön

Artikkeliin liittyvät palvelu- ja tekniikkasivut

Miksi „High Performance“ REST Delphi:ssa usein epäonnistuu rinnakkaisuuden vuoksi

Käytännössä High Performance REST-palvelinta Delphi harvoin rajoittaa pelkkä CPU-aika per pyyntö. Rajoittava tekijä on usein hallitsematon rinnakkaisuus: liian monta samanaikaista pyyntöä, liian monta samanaikaista tietokantakyselyä tai estävä I/O (tiedosto, verkko, tietokanta). Seurauksena ei synny vain „vähän hitaampaa“ suorituskykyä, vaan ketjureaktio: enemmän säikeitä, pidemmät jonot, connection-poolin romahdus, kasvavat latenssit, client-puolen time-outit ja lopulta palvelin, joka saattaa olla yhä käynnissä mutta ei anna enää stabiileja vastauksia.

Vastakeino ei ole yksittäinen temppu, vaan tietoinen overload-käyttäytyminen: kun palvelin lähestyy rajojaan, sen tulee hylätä pyynnöt varhain ja deterministisesti (tyypillisesti HTTP 429 tai 503), sen sijaan että pyynnöt ajettaisiin loputtomaan jonoon. Tätä lähdekoodikatkelmaa varten on suunniteltu kevyt Concurrency-Gate (semaphore) ja time-outit, jotka voi integroida olemassa oleviin REST-endpointteihin – riippumatta siitä, käytätkö Indyä, WebBrokeria, Horsea tai omaa HTTP-kerrosta.

Arkkitehtuuri-idea: Concurrency-Gate ennen „kalliimpaa osaa“

Perusajatus on yksinkertainen: ennen kalliimpaa osaa (tietokantahaku, monimutkaiset raportit, suuren kokoinen JSON-vastaus) varataan token semaphoresta. Jos tokenia ei ole vapaana, annetaan välittömästi kontrolloitu vastaus. Tärkeää on, että tämä portti täytyy luotettavasti vapauttaa (try/finally), ja sen on oltava siinä koodipolussa, joka on todella kallis — ei vain aivan request-handlerin alussa, jos sen jälkeen tulee vielä parser/router/autentikointi.

Tällä tavalla kuorma ei „poistu optimoimalla“, vaan kanavoidaan: palvelin vastaa samanaikaisesti vähemmän pyyntöihin, mutta latenssit pysyvät vakaampina. Yksilöllisissä yrityssovelluksissa tämä on yleensä arvokkaampaa kuin ajoittaiset parhaat ajat synteettisissä benchmarkeissa.

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

Seuraava Delphi-koodi toteuttaa Concurrency-Gaten luokkana TRestRequestGate. Se perustuu TSemaphore:iin (kuuluu System.SyncObjs-moduuliin; semaphore on laskuri rajoitetuille samanaikaisille käyttöoikeuksille). Gate-kutsu palauttaa joko „Lease“-olion (RAII-tyyppinen: vapautus destruktorissa) tai päättää välittömästi ylikuormitusvastauksen. Lisäksi mukana on hookkeja lokitusta/monitorointia varten, jotta tuotannossa näette, miksi pyynnöt hylättiin.

Delphi
unit RESTRequestGate;

interface

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

type
  // Minimkonteksti lokitusta/tracingia varten; voidaan esim. laajentaa käyttäjä- tai reittitiedoilla.
  TRESTGateContext = record
    RequestId: string;
    Route: string;
    RemoteIp: string;
  end;

  TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);

  // Hook käyttötelemetrialle (esim. tiedostoon, syslogiin, Prometheus-exporteriin jne.)
  TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
                                         Decision: TRESTOverloadDecision;
                                         WaitedMs: Integer;
                                         InFlight: Integer);

  // Lease-olio: vapauttaa tokenin destruktorissa.
  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: keine Wartezeit, sofort 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;

  // Ensin vähennetään laskuria, sitten vapautetaan semafori.
  TInterlocked.Decrement(FInFlightCounter^);
  FSemaphore.Release;
end;

{ TRESTRequestGate }

constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
  inherited Create;
  if AMaxInFlight <= 0 then
    raise EArgumentException.Create('AMaxInFlight:n on oltava > 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 (kun TimeoutMs > 0): odotettiin, mutta aikakatkaistiin.
        Decision := odRejectedTimeout;
        Result := False;
        if Assigned(FOnEvent) then
          FOnEvent(Ctx, Decision, WaitedMs, InFlight);
      end;
  else
    begin
      // wrAbandoned/virhetilanteet: hylätään konservatiivisesti
      Decision := odRejectedBusy;
      Result := False;
      if Assigned(FOnEvent) then
        FOnEvent(Ctx, Decision, WaitedMs, InFlight);
    end;
  end;
end;

end.

Tarkoitus: vakaus kuormituksessa, ei „kaikki samalla kertaa”

Käyttämällä MaxInFlight määrittelet, kuinka monta pyyntöä saa samanaikaisesti edetä „kalliseen osaan“. Tämä ei ole tarkoituksella „CPU-ytimien määrä“, vaan operatiivinen mitattava raja. Tietokantapainotteisilla endpointeilla on usein järkevää asettaa MaxInFlight suhteessa DB-Connection-Pooliin (esimerkiksi Pool = 20, MaxInFlight = 12–16), jotta jokainen pyyntö ei lukitse yhteyttä ja muut säikeet eivät ala jonottaa.

Reunaehdot ja kompastuskivet

  • Try/Finally ist Pflicht: Lease on vapautettava ehdottomasti. Jos endpointissa esiintyy poikkeuksia, portti voi „vuotaa“ ja palvelin jää pysyvästi „busy“-tilaan.
  • Timeout sinnvoll wählen: TimeoutMs=0 on kova raja (hylätään välittömästi). Lyhyt timeout (tyypillisesti 50–150 ms) tasoittaa kuormahuippuja ilman, että syntyy todellisia jonotusseisokkeja.
  • Gate nicht zu früh: Autentikointi (esim. Bearer/JWT) tai reititys voi olla edullinen paikka; semaforin tulisi astua voimaan ennen varsinaista kallista vaihetta. Toisaalta: jos autentikointi on kallista (esim. ulkoinen Identity-järjestelmä), se on myös rajoitettava.
  • 429 vs 503: HTTP 429 („Too Many Requests“) sopii hyvin, kun asiakkaat yrittävät uudelleen kontrolloidusti. 503 („Service Unavailable“) on sopiva, kun palvelu ei tilapäisesti pysty ottamaan pyyntöjä vastaan järkevästi. Molemmissa tapauksissa Retry-After-header on suositeltava.

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

Koodi on tarkoituksella framework-neutraali. Tarvitsette vain paikan, jossa pyynnöt „kulkevat läpi“. Tyypillisesti käytetään globaalia singletonia tai gatea per reittiryhmä (esim. „/reports“ pienempi gate, „/health“ ilman gatea). Esimerkkinä liittäminen malliksi:

  • Täytä konteksti (RequestId, Route, RemoteIp)
  • TryAcquire lyhyellä timeoutilla
  • Hylkäyksen yhteydessä kirjoita vastaus välittömästi (429/503) ja lopeta
  • Lease on voimassa scopessa kunnes kallis osa on suoritettu

Horsessa (middleware) gate on lähellä reittiryhmää. WebBrokerissa voit toimia kunkin action-handlerin sisällä. Indyn tapauksessa riippuu siitä, onko jokaiselle pyynnölle oma thread; gate vaikuttaa silti, kunhan kalliit osiot on selkeästi rajattu.

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

Ylikuormitusvastaukset eivät ole pelkkiä status-koodeja. Jos asiakkaat reagoivat 429/503-koodiin lähettämällä heti aggressiivisesti uudet pyynnöt, syntyy retry-myrsky. Heterogeenisessa järjestelmämaisemassa (Mobile Apps, C# Services, legacy-asiakkaat) auttaa yhtenäinen käytös:

  • Retry-After: esimerkiksi 1–3 sekuntia riippuen endpointista. Tämä antaa selkeän rytmin.
  • Kurzer Body: Pieni JSON kuten {"error":"server_busy","requestId":"..."} riittää. Suuret error-objektit kuluttavat taas CPU:ta ja kaistanleveyttä.
  • Health-Endpoint ungedrosselt: Monitoroinnin tulee antaa tietoa myös kuormituksessa (tarvittaessa „degraded“-lipun kanssa).

Jos käytät ennen sovellusta reverse-proxyä kuten nginx:ssä, säädä siellä timeoutit ja buffering vastaavasti. Proxy voi keventää kuormaa (TLS-termination, Keep-Alive), mutta siirtää myös kuormitusta (esim. suurten request-bodyjen puskurointi). Käytännössä on tärkeää, että rajat ovat johdonmukaiset: Proxy-Timeout > App-Timeout, muuten asiakkaat saattavat nähdä „Gateway Timeout“, vaikka sovellus olisi hylännyt pyynnön hallitusti.

Threading, DB-poolit ja Keep-Alive: missä käytännössä pettää

Gate ratkaisee „liian monta samanaikaisesti“ -ongelman, mutta se ei estä automaattisesti sitä, että yksittäinen pyyntö sitoo liikaa resursseja. Kolme tyypillistä murtumakohtaa Delphi-projekteissa syntyy juuri rajapinnoissa säikeistämisen, tietokannan ja HTTP-yhteyksien välillä:

  • Yksi pyyntö estää useita rajallisia resursseja: Ensin DB-yhteys, sitten ulkoinen HTTP-kutsu, sitten tiedostokäsittely. Jos kaikki tapahtuu samassa pyyntösäikeessä, estoaika moninkertaistuu. Gate rajoittaa silloin toki rinnakkaisuutta, mutta läpimeno heikkenee voimakkaasti. Tässä kannattaa irrottaa riippuvuuksia (esim. ulkoiset kutsut asynkronisesti, esivalmistelu job-jonolla).
  • BDE-korvaus natiiviliitännällä – poolaus ja transaktiot: BDE-Ablosung mit nativer Anbindung voi pitää Connections poolissa, mutta „pitkä“ transaktio (esim. koska JSON:n muodostus tai liiketoimintatarkistukset ovat StartTransaction ja Commit välissä) pitää yhteyden tarpeettomasti varattuna. Hyvä käytäntö on rajata transaktio mahdollisimman tiukasti varsinaisten lausuntojen ympärille ja sarjoittaa tai validoida muu logiikka transaktion ulkopuolella, kun se on toiminnallisesti mahdollista.
  • HTTP Keep-Alive piilotettuna muistinsyöjänä: Keep-Alive vähentää handshakereita, mutta monien lepotilassa olevien asiakkaiden tapauksessa se voi johtaa liian moniin avoimiin socketteihin. Erityisesti Windows- ja Linux-palveluissa ei näy „CPU ylös“, vaan „Handles/FDs täynnä“ tai muistin kasvu puskurien takia. Tässä auttavat selkeät idle-timeoutit palvelimella ja reverse-proxyssä sekä rajoitus per client-IP, jos ympäristö sen sallii.

Seurauksena: MaxInFlight ei ole staattinen arvo. Se riippuu hitaimmasta, rajallisimmasta resurssistanne (DB, ulkoiset järjestelmät, storage) ja siitä, kuinka hyvin pyyntö „pitää“ näitä resursseja yhdessä.

Suorituskyvyn vivut Gate:n lisäksi: JSON, DB ja I/O eivät saa sekoittua

Gate stabiloi, mutta se ei korvaa selkeää endpointien resurssitaloutta. Kolme hidastavaa tekijää Delphi REST-palvelimissa toistuvat:

  • JSON:n rakentaminen tarpeettomilla välivaihemerkkijonoilla: Kuorma syntyy usein monista tilapäisistä Unicode-merkkijonoista. Missä mahdollista, rakentakaa streaming-tekijöin (Writer/Stream) sen sijaan, että luotte valtavia välivaihe-objekteja — erityisesti listauksia tarjoavissa endpointissa.
  • Tietokantakyselyt „per item“: N+1-kyselyt ja rivikohtaiset lookupit ovat klassikko. Parempi: kohdennetut joinit, batch-kyselyt, palvelinpuolen aggregointi. Erittäin suurten tulosten kohdalla kannattaa lisäksi käyttää sivutusta vakaalla lajittelulla (jotta sivut eivät „hyppää“).
  • Estävä I/O pyyntöä säikeessä: Tiedostokäsittelyt tai ulkoiset HTTP-kutsut tulisi joko tiukasti rajoittaa tai siirtää asynkroniseen putkeen. Muuten sitotte kalliita säikeitä „odottamiseen“.

Kasvaneissa digitaalisissa yritysjärjestelmissä tämä on usein kynnyskohta: endpoint lisättiin „pikaisesti“ ja toimii, kunnes todelliset kuormat ja datamassat tulevat. Silloin paljastuu, onko arkkitehtuurin rajat vedetty puhtaasti (tietokantakerros, caching, erästrategiat, selkeät timeoutit).

Debuggaus ja operointi: mitä kannattaa mitata

Hook OnEvent on tietoisesti yksinkertainen. Käytännössä kannattaa kerätä vähintään seuraavat arvot:

  • InFlight (nykyinen rinnakkaisuus Gatessa)
  • WaitedMs (kuinka paljon jonoutumista sallitte)
  • Decision (accepted/busy/timeout)
  • Route/RemoteIp (karkeaan juurisyyn analyysiin, kuitenkaan tietosuojaa unohtamatta)

Tämän avulla saatte signaalin siitä, ovatko rajat liian tiukat (liian monta 429) vai liian löysät (korkeat WaitedMs, kasvavat latenssit). Näette myös, hallitsevatko yksittäiset reitit kuormaa. Windows- ja Linux-Services -ympäristöissä tämä on arjessa ratkaisevaa: ilman telemetriaa suorituskykyongelmasta tulee nopeasti arvauspeliä verkon, tietokannan, proxyn ja sovelluksen välillä.

Epätavallista, mutta erittäin hyödyllistä: „WaitedMs“ varhaisen varoituksen indikaattorina

Monet tiimit katsovat vain vastausaikaa ja CPU:ta. WaitedMs on usein parempi indikaattori, koska se näyttää, että pyynnöt odottavat jo ennen varsinaista työtä. Jos WaitedMs nousee samalla kun CPU pysyy kohtuullisena, rajallinen resurssi ei usein ole CPU vaan jokin pool (DB-yhteydet), lukko liiketoimintalogiikassa tai ulkoinen downstream-palvelu. Se säästää aikaa juurisyyn analyysissä, koska voitte hakea kohdennetummin kohti „Pool/Lock/I/O“ sen sijaan, että etsisitte „kääntäjäoptimointia“.

Vaihtoehdot: reitikohtaiset portit, prioriteetit ja „Fast Lane“

Yksi portti kaikkeen on yksinkertaista, mutta ei aina ihanteellista. Käytännöllisiä vaihtoehtoja:

  • Reittiryhmäkohtainen portti: „/reports“ tiukka, „/api/orders“ kohtalainen, „/health“ avoin. Näin estätte, että kalliit raporttipyynnöt syrjäyttävät ydintoiminnot.
  • Fast Lane ylläpidolle/monitorointiin: erillinen portti pienellä samanaikaisuusluvulla, jotta käyttötoimet ovat mahdollisia myös kuormituksessa.
  • Budjettiin perustuvat rajat: Jos vastauskoot vaihtelevat paljon, tavubudjetti voi auttaa (esim. enintään X MB samanaikaisesti luomisessa). Tämä on monimutkaisempi, mutta realistinen suurissa latauksissa.

Tärkeää: priorisointi muuttuu nopeasti poliittiseksi („minun endpointini on tärkeämpi“). Tekninen vakaus säilyy, kun prioriteetit kytketään prosesseihin (esim. tilauksen kirjaus ennen raportointia), eivät rooleihin tai osastoihin.

Yhteenveto: Kannattaako portti — ja missä kohdissa lähestymistapa pettää?

Concurrency-Gate on pragmaattinen osa High Performance REST -palvelinta Delphi:ssa, koska se tekee ylikuormituksen hallittavaksi ja pitää järjestelmänne vakaana huippukuormassa. Se kannattaa erityisesti, jos teillä on tietokantariippuvaisia endpointteja, jos sen edessä on reverse proxy tai jos useat clientit (Legacy, portaalit, Services) tuottavat kuorman aalloissa.

Rajat ovat selvät: jos varsinainen työ per pyyntö on liian kallis (tehottomat kyselyt, suuret JSON-objektit, estävät ulkoiset järjestelmät), portti peittää vain oireita. Silloin tietokantakutsut, välimuististrategiat, aikakatkaisut ja tarvittaessa asynkroninen käsittely (Queue/Job-System) on vietävä kuntoon. Käyttöympäristön turvavyönä portti on kuitenkin usein ero „vähän hitaan“ ja „täysin käyttökelvottoman“ välillä.

Jos haluatte ottaa ylikuormituskäyttäytymisen käyttöön olemassa olevaan Delphi REST-API ja REST-Server -ympäristöön tai tasapainottaa rajat tietokanta- ja proxy-aikakatkaisuilla siististi, keskustelkaa projektista tai modernisointihankkeesta Net-Base.

Ammatillisessa kontekstissa myös Thread-Pool Delphi ja Http 429 Too Many Requests näyttelevät tärkeää roolia, kun integraatioiden, tietovirtojen ja jatkokehityksen on toimittava yhteen siististi.

Keskustele projektista tai modernisointihankkeesta Net-Base kanssa.

Seuraava vaihe

Kun aiheesta tulee todellinen projekti, arkkitehtuuri, nykyinen järjestelmäkanta ja käyttö tulisi varhaisessa vaiheessa tarkastella yhdessä.

Emme tue pelkästään yksittäiskysymyksissä, vaan myös silloin, kun lähdekoodipalasista, legacy-aiheista tai portaali-ideoista halutaan muodostaa luotettava yrityshanke.

  • Nykytila, tavoitetila ja tekniset riskit arvioidaan yhdessä.
  • REST, datan käyttö, portaalit ja käyttöönotto eivät jätetä myöhempien seurausten varaan.
  • Näette ajoissa, mikä ratkaisu on taloudellisesti ja toiminnallisesti kestävä.

Jaa artikkeli

Jaa tämä viesti suoraan

LinkedIn, X, XING, Facebook, WhatsApp ja sähköposti ovat heti käytettävissä. Instagramia varten valmistelemme linkin ja lyhyen tekstin.

Sähköposti

Instagram avautuu uuteen välilehteen. Linkki ja lyhyt teksti kopioidaan ensin leikepöydälle.