Nuo žurnalo temos iki projekto įgyvendinimo
Tinkami puslapiai apie paslaugas ir techninę informaciją šiam įrašui
Kodėl „High Performance“ su REST sistemoje Delphi dažnai žlunga dėl nekontroliuojamo lygiagretumo
Praktikoje High Performance REST serveris Delphi retai yra ribojamas vien tik CPU laiku vienai užklausai; dažniau jis stabdomas nekontroliuojamo lygiagretumo: per daug vienu metu vykstančių užklausų, per daug vienu metu vykdomų duomenų bazės užklausų arba blokuojantis I/O (failai, tinklas, duomenų bazė). Rezultatas pasireiškia ne kaip „šiek tiek lėtesnis“, o kaip grandininė reakcija: daugiau gijų, ilgesnės laukimo eilės, Connection-Pool kolapsas, didėjančios latenčijos, klientų pusės timeout’ai ir galų gale serveris, kuris galbūt dar „gyvas“, bet nebeteikia stabilių atsakymų.
Priešnuodis nėra vienas triukas, o sąmoningas Overload-Verhalten: kai serveris pasiekia savo ribas, jis turi anksti ir deterministiškai atmesti užklausas (įprastai HTTP 429 arba 503), užuot leidęs užklausoms kauptis begalinėje laukimo eilėje. Būtent tam skirtas šis kodo fragmentas: lengvas Concurrency-Gate (Semaphore) kartu su laiko limitais, kurį galima integruoti į esamus REST endpoint’us – nepriklausomai nuo to, ar naudojate Indy, WebBroker, Horse ar savo HTTP sluoksnį.
Architektūrinė idėja: Concurrency-Gate prieš „brangųjį“ darbą
Pagrindinė idėja paprasta: prieš brangųjį darbą (duomenų bazės pasiekiamumas, sudėtingos ataskaitos, dideli JSON atsakymai) rezervuojamas žetonas iš Semaphore. Jei žetono nėra, grąžinama akimirksniu kontroliuojama atsakymas. Svarbu: šis gate’as turi būti patikimai atlaisvintas (try/finally), ir jis turi būti įtrauktas į tą kodo šaką, kuri iš tikrųjų yra brangi – ne tik pačioje request handlerio pradžioje, jei po to vis tiek vyksta parseris/routeris/autentifikacija.
Taip apkrova nėra „išoptimizuojama“, o kanalizuojama: serveris atsako į mažiau užklausų vienu metu, bet su stabilesnėmis latenčiomis. Individualiose įmonės taikomosiose programose tai dažniausiai yra vertingiau nei sporadiniai geriausi rezultatai sintetiniuose benchmarkuose.
Kodo fragmentas: užklausų ribotuvas su laiko limitu, 429/503 ir telemetrijos hook’ais
Žemiau pateiktas Delphi-kodas įgyvendina Concurrency-Gate klasės TRestRequestGate pavidalu. Jis remiasi TSemaphore (iš System.SyncObjs; semaphore yra skaitiklis ribotam vienalaikių prieigų skaičiui). Gate kvietimas grąžina arba „Lease“ objektą (panašiai į RAII: atlaisvinimas destruktoriuje), arba nusprendžia dėl akimirksniu grąžinamos perkrovos atsakymo. Papildomai yra hook’ai logging/monitoringo reikmėms, kad eksploatacijos metu matytumėte, kodėl užklausos buvo atmetamos.
unit RESTRequestGate;
interface
uses
System.SysUtils,
System.Classes,
System.SyncObjs,
System.Diagnostics;
type
// Minimalus kontekstas žurnalavimui/sekai; pvz., galima išplėsti apie vartotoją/maršrutą.
TRESTGateContext = record
RequestId: string;
Route: string;
RemoteIp: string;
end;
TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);
// Hook operacijos telemetrijai (pvz., įrašymas į failą, Syslog, Prometheus eksportuotojas ir kt.)
TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
Decision: TRESTOverloadDecision;
WaitedMs: Integer;
InFlight: Integer);
// Lease objektas: žetono atlaisvinimas destruktoriuje.
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: jokio laukimo, iškart 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;
// Pirmiausia sumažinti skaitiklį, tada atlaisvinti semaforą.
TInterlocked.Decrement(FInFlightCounter^);
FSemaphore.Release;
end;
{ TRESTRequestGate }
constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
inherited Create;
if AMaxInFlight <= 0 then
raise EArgumentException.Create(‚AMaxInFlight turi būti > 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 esant TimeoutMs > 0: laukti, bet riboti laiką.
Decision := odRejectedTimeout;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
else
begin
// wrAbandoned/Fehlerfälle: konservatyviai atmesti
Decision := odRejectedBusy;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
end;
end;
end.
Paskirtis: stabilumas esant apkrovai, o ne „viskas vienu metu“
Naudodami MaxInFlight nurodote, kiek užklausų vienu metu gali patekti į „brangųjį“ skyrių. Tai sąmoningai nėra „Anzahl CPU-Kerne“, o eksploatacinis parametras. Duomenų bazės apkrovai jautriems endpointams dažnai prasminga MaxInFlight suderinti su DB-Connection-Pool (pvz., Pool = 20, MaxInFlight = 12 iki 16), kad kiekviena užklausa neužblokuotų jungties ir nepritrauktų papildomų gijų.
Apribojimai ir spąstai
- Try/Finally yra privalomas: Lease turi būti garantuotai atleistas. Jei endpoint’e įvyksta Exceptions, kitaip vartai taps „undicht“ ir serveris nuolat liks „busy“.
- Timeoutą pasirinkite protingai:
TimeoutMs=0yra griežtas limitas (užklausos atmetamos iš karto). Trumpas laiko limitas (įprastai 50 iki 150 ms) išlygina pikus, nesukuriant tikrų laukimo eilių. - Vartų nedėkite per anksti: Autentifikacija (pvz. Bearer/JWT) arba routing gali būti pigūs; semaforas turėtų blokuoti prieš tikrai brangų skyrių. Priešingai: jei auth tampa brangus (pvz. prieš išorinę identiteto sistemą), jis taip pat turi būti ribojamas.
- 429 vs 503: HTTP 429 („Too Many Requests“) tinka, kai klientai turi vykdyti pakartotinius bandymus. 503 („Service Unavailable“) tinka, kai paslauga laikinai iš esmės negali priimti užklausų. Abu atvejais rekomenduojama
Retry-Afterantraštė.
Integracija į REST-Handler: Indy/WebBroker/Horse pragmatiškai
Šis fragmentas sąmoningai yra framework-neutralus. Reikia tik vienos vietos, kur užklausos „praeina“. Įprastai tai būna globalus singleton arba vartai kiekvienai maršruto grupei (pvz., „/reports“ su mažesniu limitu, „/health“ be vartų). Pavyzdinis įtraukimas kaip modelis:
- Užpildyti kontekstą (RequestId, Route, RemoteIp)
TryAcquiresu trumpu laiko limitu- Atmetus, iš karto parašyti atsakymą (429/503) ir baigti
- Lease gyvuoja scope ribose iki brangiosios dalies pabaigos
Horse (Middleware) atveju vartai yra arti maršruto grupės. WebBroker galite dirbti atitinkamame Action-Handler. Indy atveju tai priklauso nuo to, ar turite po vieną giją kiekvienai užklausai; vartai vis tiek veiks, kol brangiosios sekcijos bus aiškiai apribotos.
Aukštos našumo REST Server Delphi: perkrovos atsakymai, kurie ne „vergiften“ klientų
Perkrovos atsakymai yra daugiau nei statuso kodai. Jei klientai prie 429/503 agresyviai iš karto siunčia pakartotinai, susidaro retry audra. Heterogeninėse sistemų aplinkose (mobiliosios programėlės, C# Services, Legacy-Clients) padeda nuoseklus elgesys:
- Retry-After: pavyzdžiui 1 iki 3 sekundžių, priklausomai nuo endpointo. Tai aiškus ritmo signalas.
- Trumpas atsako turinys: Mažas JSON, pvz.
{"error":"server_busy","requestId":"..."}, užtenka. Dideli klaidų objektai vėl kainuoja CPU ir pralaidumą. - Health-Endpoint neapkarpytas: Monitoringo įrašai turi teikti informaciją net prie apkrovos (pvz. su „degraded“ žyma).
Jei priešais dedate reverse proxy, pvz. nginx: suderinkite Timeouts ir Buffering ten. Proxy gali atleisti apkrovą (TLS-Termination, Keep-Alive), bet taip pat gali apkrovą perkelti (pvz. talpindamas didelius Request-Bodies). Eksploatacijoje svarbu, kad limitai būtų nuoseklūs: Proxy-Timeout > App-Timeout, kitaip klientai matys „Gateway Timeout“, nors programa būtų tinkamai atmetusi užklausą.
Threading, DB-Pools ir Keep-Alive: kur praktiškai viskas lūžta
Gate išsprendžia „per daug vienu metu“ problemą, bet savaime neapsaugo nuo to, kad vienas Request nepririštų per daug resursų. Trys tipiniai lūžio taškai iš Delphi-projektų susidaro būtent sąsajose tarp threading, duomenų bazės ir HTTP jungčių:
- Ein Request blockiert mehrere knappe Ressourcen: Pirmiausia DB‑prisijungimas, paskui išorinis HTTP‑call, paskui failo prieiga. Jeigu visa tai vyksta toje pačioje užklausos gijoje, blokuojančio laiko efektas daugėja. Gate tada apriboja paraleliškumą, bet pralaidumas krenta drastiškai. Čia verta atsieti priklausomybes (pvz. išorinius kvietimus vykdyti asinchroniškai, išankstinį skaičiavimą per darbo eilę / 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. Gera praktika – apgaubti transakciją kuo siauresniu rėmu aplink pačius statements ir, jei įmanoma fachlich, serializuoti arba validuoti už transakcijos ribų.
- HTTP Keep-Alive als versteckter Speicherfresser: Keep-Alive mažina rankų paspaudimus, bet prie daugelio tuščiojo režimo klientų gali sukelti per daug atvirų soketų. Ypač Windows- und Linux-Services atveju matote ne „CPU aukštai“, o „Handles/FDs pilna“ arba atminties augimą dėl buferių. Padeda aiškūs idle‑timeout’ai serveryje ir reverse proxy bei limitas per klientų IP, jeigu aplinka tai leidžia.
Konsekvencija: MaxInFlight nėra statiška reikšmė. Ji priklauso nuo jūsų lėčiausio, siauriausio resurso (DB, išorinės sistemos, Storage) ir nuo to, kaip gerai vienas Request šiuos resursus „laiko kartu“.
Performance-Hebel neben dem Gate: JSON, DB und I/O nicht vermischen
Gate stabilizuoja, bet nepakeičia tvarkingos endpoint‑ūkonomikos. Trys dažnai pasikartojantys stabdžiai Delphi REST‑serveriuose yra šie:
- JSON-Building mit unnötigen Zwischenstrings: Apkrovą dažnai sukuria daugybė laikinų Unicode eilučių. Kur įmanoma, konstruokite orientuotai į srautą (Writer/Stream) vietoje didelių tarpinių objektų, ypač sąrašų endpointuose.
- Datenbankzugriff „pro Item“: N+1‑Queries ir per‑eilutės lookup’ai yra klasika. Geriau: tikslingi Joins, batch‑Queries, serverio pusės agregacija. Esant labai dideliems rezultatams verta papildomai naudoti puslapiavimą su stabiliu rūšiavimu (kad puslapiai ne „šokinėtų“).
- Blockierende I/O im Request-Thread: Failo prieigos arba išoriniai HTTP‑Calls turėtų būti arba griežtai ribojami, arba perkelti į asinchroninę pipeline. Priešingu atveju brangios gijės blokuojamos „laukimui“.
Augančioms skaitmeninėms verslo sistemoms tai dažnai ir yra esminė problema: endpointas buvo „greitai“ pridėtas ir veikia, kol ateina reali apkrova ir duomenų apimtys. Tuomet paaiškėja, ar architektūrinės ribos buvo aiškiai nubrėžtos (duomenų prieigos sluoksnis, Caching, Bulk‑strategijos, aiškūs Timeouts).
Debugging und Betrieb: Was Sie messen sollten
Der Hook OnEvent yra sąmoningai paprastas. Praktikoje verta fiksuoti bent šias reikšmes:
- InFlight (aktuelle Parallelität am Gate)
- WaitedMs (wie viel „Queueing“ Sie zulassen)
- Decision (accepted/busy/timeout)
- Route/RemoteIp (bendro pobūdžio priežasčių analizė, nepažeidžiant duomenų apsaugos)
Tai suteikia signalą, ar limitai per griežti (per daug 429) ar per silpni (didelis WaitedMs, augančios latenčios). Ir matote, ar atskiros maršruto kryptys dominuoja. Für Windows- und Linux-Services tai kasdienėje veikloje yra lemiama: be telemetrijos našumo problema greitai virsta spėliojimu tarp tinklo, duomenų bazės, proxy ir aplikacijos.
Neįprasta, bet itin naudinga: „WaitedMs“ kaip ankstyvojo įspėjimo indikatorius
Daugelis komandų žiūri tik į Response-Time ir CPU. WaitedMs dažnai yra geresnis indikatorius, nes jis rodo, kad užklausos jau laukiamos prieš pačią apdorojimo dalį. Jei WaitedMs kyla, o CPU išlieka vidutinė, trūkstama išteklių dažnai nėra CPU, o pool (DB-Verbindungen), užraktas verslo logikoje arba išorinis downstream-service. Tai sutaupo laiko priežasčių analizei, nes leidžia taikliau ieškoti link „Pool/Lock/I/O“ vietoje „Compiler-Optimierung“.
Variantai: per-maršruto vartai, prioritetai ir „Fast Lane“
Vieni vartai viskam yra paprasta, bet ne visuomet idealu. Pagrįsti variantai:
- Vartai pagal maršrutų grupę: „/reports“ griežtai, „/api/orders“ vidutiniškai, „/health“ atviri. Taip išvengsite, kad resursų reikalaujančios ataskaitų užklausos išstumtų pagrindinius procesus.
- Fast Lane administravimui/monitoringui: atskiri vartai su mažu paraleliškumo limitu, kad operatyvinės veiklos būtų įmanomos net esant apkrovai.
- Biudžetu pagrįsti limitai: jei atsakymų dydžiai stipriai svyruoja, papildomas baitų biudžetas gali padėti (pvz. maksimaliai X MB vienu metu generuojant). Tai sudėtingiau, bet realistiška esant dideliems downloadams.
Svarbu: prioritetų nustatymas greitai tampa politiškas („mano endpoint’as yra svarbesnis“). Techniniu požiūriu stabilu, kai prioritetai yra susieti su procesais (pvz. užsakymų įvedimas prieš ataskaitavimą), o ne su vaidmenimis ar departamentais.
Išvada: ar verta vartai – ir kada požiūris žlunga?
Concurrency-Gate yra pragmatiškas komponentas High Performance REST serveriui Delphi, nes jis leidžia valdyti perkrovą ir palaikyti sistemų stabilumą piko apkrovose. Ypač naudingas, jei turite duomenų bazei priklausančius endpoint’us, jei priešais yra reverse proxy arba jei keli klientai (Legacy, portalai, Services) bangomis generuoja apkrovą.
Ribos aiškios: jei pati užklausos apdorojimo dalis yra per daug brangi (neefektyvūs užklausimai, dideli JSON objektai, blokuojančios išorinės sistemos), vartai tik maskuoja simptomus. Tada reikia tobulinti duomenų prieigą, kešavimo strategijas, timeout’us ir, prireikus, asinchroninį apdorojimą (Queue/Job-System). Kaip saugos diržas eksploatacijoje, vartai dažnai yra skirtumas tarp „truputį lėtas“ ir „visiškai nenaudingas“.
Jei norite įtraukti overload-elgseną į esamą Delphi REST-API und REST-Server arba kruopščiai subalansuoti limitus su duomenų bazės ir proxy timeout’ais: aptarkite projektą arba modernizacijos užduotį su Net-Base.
Profesinėje aplinkoje taip pat svarbų vaidmenį atlieka Thread-Pool Delphi ir Http 429 Too Many Requests, kai integracijos, duomenų srautai ir tolesnė plėtra turi veikti darniai.
Kitas žingsnis
Kai tema virsta realiu projektu, architektūra, esami sprendimai ir eksploatavimas turėtų būti nagrinėjami kartu nuo pat pradžių.
Mes padedame ne tik pavienėse užklausose, bet ir tuomet, kai iš šaltinio kodo fragmentų, paveldėtų temų ar portalo idėjų turi tapti patikimas įmonės projektas.
- Esama padėtis, tikslinis vaizdas ir techninės rizikos vertinami kartu.
- REST, duomenų prieiga, portalai ir rollout nebus perkelti į vėlesnį etapą kaip vėlyvos pasekmės.
- Jūs anksti matote, kuris kelias yra ekonomiškai ir operaciniškai tvarus.