Од теме часописа до пројектне праксе
Одговарајуће странице услуга и техничке странице за чланак
Зашто „High Performance“ код REST у Delphi често не успева због паралелности
Ein High Performance REST Server Delphi je у пракси ретко лимитиран само чистим CPU-vremenom по захтеву, већ неконтролисаном паралелношћу: превише истовремених захтева, превише истовремених упита ка бази података или блокирајући I/O (датотека, мрежа, база). Резултат онда не делује као „мало спорије“, већ као ланчана реакција: више нити, више редова чекања, колапс Connection-Pool-а, растуће латенције, timeouts на страни клијента и на крају сервер који још „живи“, али више не даје стабилне одговоре.
Противмерa није један трик, већ свесно Overload-Verhalten: када сервер дође до својих граница, мора рано и детерминистички да одбацује (типично HTTP 429 или 503), уместо да пусти захтеве у бесконачан ред чекања. Управо за то је овај Source-Schnipsel намењен: лак и лаган Concurrency-Gate (Semaphore) плус Timeouts, који се може интегрисати у постојеће REST-ендпоите — без обзира да ли користите Indy, WebBroker, Horse или сопствени HTTP-слој.
Архитектонска идеја: Concurrency-Gate испред „скупог дела“
Основна идеја је проста: пре скупог дела (приступ бази података, комплексни извештаји, велики JSON-одговори) резервише се token из семафора. Ако нема слободног token-а, одговор је одмах контролисан. Важно је: ова врата морају бити поуздано ослобођена (try/finally), и морају бити уграђена у ток кода који је заиста скуп — не само на самом почетку Request-Handler-а, ако након тога ионако следе парсер/router/autentifikacija.
На тај начин се оптерећење не „отуђује“, већ каналише: сервер одговара на мање захтева истовремено, али са стабилнијим латенцијама. У индивидуалним пословним апликацијама ово је обично вредније од појединачних најбољих резултата у синтетичким бенчмарковима.
Source-Schnipsel: Request-Limiter mit Timeout, 429/503 und Telemetrie-Hooks
Наведени Delphi-код имплементира Concurrency-Gate као класу TRestRequestGate. Он се заснива на TSemaphore (из System.SyncObjs; семафор је бројач за ограничене истовремене приступе). Позив Gate-а враћа или „Lease“-објекат (слично RAII: ослобађање у деструктору) или одлучује за тренутни одговор о преоптерећењу. Додатно постоје hook-ови за логовање/мониторинг, како бисте у раду видели почему су захтеви били одбијени.
unit RESTRequestGate;
interface
uses
System.SysUtils,
System.Classes,
System.SyncObjs,
System.Diagnostics;
type
// Минимални контекст за логовање/трејсинг; може се нпр. проширити за корисника/руту.
TRESTGateContext = record
RequestId: string;
Route: string;
RemoteIp: string;
end;
TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);
// Hook за оперативну телеметрију (нпр. у фајл, Syslog, Prometheus-експортер итд.)
TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
Decision: TRESTOverloadDecision;
WaitedMs: Integer;
InFlight: Integer);
// Lease-објекат: ослобађање токена у деструктору.
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: нема чекања, одмах 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;
// Прво умањити бројач, затим ослободити семафор.
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 код TimeoutMs > 0: циљано чекање, али ограничено.
Decision := odRejectedTimeout;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
else
begin
// wrAbandoned/грешке: конзервативно одбити
Decision := odRejectedBusy;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
end;
end;
end.
Сврха: стабилност под оптерећењем уместо „све одједном“
Pomoću MaxInFlight definišete koliko zahteva istovremeno sme da uđe u „skupi deo“. To svesno nije „broj CPU jezgara“, već operativna veličina. Za endpointe koji su opterećени bazom podataka često je smisleno postaviti MaxInFlight u odnosu na pool veza ka bazi (na primer Pool = 20, MaxInFlight = 12 do 16), tako da ne bi svaki zahtev blokirao vezu i potom povlačio dodatne niti.
Ograničenja i zamke
- Try/Finally је обавезан: Lease мора бити гарантовано ослобођен. Ако имате Exceptions у endpoint-у, иначе ће Gate пропустити и сервер ће остати трајно у стању „busy“.
- Изаберите смислен Timeout:
TimeoutMs=0је оштра граница (одмах одбити). Кратак timeout (типично 50 до 150 ms) ублажава пиковања без формирања правих редова чекања. - Не постављајте Gate прерано: Аутентикација (на пример Bearer/JWT) или рутинг може бити јефтинији; семафор би требао захватити пре заиста скупог дела. Супротно: ако аутентификација постане скуп посао (нпр. према спољном систему за идентификацију), и она мора бити ограничена.
- 429 против 503: HTTP 429 („Too Many Requests“) је погодан када клијенти треба да намерно пошаљу нови покушај. 503 („Service Unavailable“) је прикладан када услуга привремено уопште није у стању да смислено прими захтеве. У оба случаја препоручује се
Retry-Afterзаглавље.
Integration in REST-Handler: Indy/WebBroker/Horse прагматично
Овај снипет је намерно framework-neutral. Потребно вам је само место где захтеви „пролазе“. Типично је глобални Singleton или Gate по групи рута (на пример „/reports“ мањи, „/health“ без Gate). Пример интеграције као образац:
- Попунити контекст (RequestId, Route, RemoteIp)
TryAcquireса кратким timeout-ом- При одбијању одмах вратити одговор (429/503) и завршити
- Lease остаје у опсегу до завршетка скупог дела
У Horse (Middleware) Gate се налази близу групе рута. У WebBroker-у можете радити у одговарајућем Action-Handler-у. Код Indy зависи да ли имате по једну нит по захтеву; Gate ипак делује, све док су скупи делови јасно ограничени.
Високоперформантни REST сервер Delphi: одговори при преоптерећењу који не „отрују“ клијенте
Одговори при преоптерећењу су више од статусних кодова. Ако клијенти при 429/503 агресивно одмах пошаљу поново захтев, добићете олују поновних покушаја. У хетерогеној системској средини (мобилне апликације, C# услуге, legacy клијенти) помаже доследно понашање:
- Retry-After: на пример 1 до 3 секунде, у зависности од endpoint-а. То је јасан сигнал.
- Кратак body: Мали JSON као
{"error":"server_busy","requestId":"..."}је довољан. Велики објекти грешке поново троше CPU и пропусни опсег. - Health-Endpoint без ограничења: Мониторинг треба да даје информације и под оптерећењем (по потреби са „degraded“-флагом).
Ако користите реверс-прокси као nginx испред апликације: усагласите тамо timeoute и buffering. Прокси може растеретити (TLS-Termination, Keep-Alive), али и померити оптерећење (нпр. баферовати велике request-body-је). У раду је кључно да лимити буду конзистентни: Proxy-Timeout > App-Timeout, иначе клијенти виде „Gateway Timeout“, иако је апликација коректно одбила захтев.
Threading, DB-Pools und Keep-Alive: Wo es in der Praxis kippt
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:
- 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. Das 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-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. 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
Das 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 (gruba analiza uzroka, bez ignorisanja zaštite podataka)
To vam daje signal da li su limiti previše strogi (previše 429) ili preslabi (visok WaitedMs, rastuće latencije). Takođe vidite da li pojedinačne rute dominiraju. Za Windows- i Linux-Services je ovo u svakodnevnom radu presudno: bez telemetrije problem sa performansama brzo postane pogađanje između mreže, baze podataka, proxyja i aplikacije.
Neobično, ali izuzetno korisno: „WaitedMs“ kao indikator ranog upozorenja
Mnogi timovi gledaju samo Response-Time i CPU. WaitedMs često je bolji indikator, jer pokazuje da zahtevi već čekaju pre same obrade. Ako WaitedMs raste dok CPU ostaje umerena, usko grlo često nije CPU, već neki pool (DB-veze), lock u poslovnoj logici ili eksterni downstream-servis. To štedi vreme pri analizi uzroka jer omogućava ciljano traženje prema „Pool/Lock/I/O“ umesto prema „optimizaciji kompajlera“.
Varijante: Gate-ovi po ruti, prioriteti i „Fast Lane“
Jedno gate za sve je jednostavno, ali ne uvek idealno. Smisleni pristupi:
- Gate po grupi ruta: „/reports“ strogo, „/api/orders“ umereno, „/health“ otvoreno. Tako sprečavate da skupi zahtevi za izveštajima istisnu ključne procese.
- Fast Lane za Admin/Monitoring: Odvojeno gate sa malom paralelnošću, kako bi administrativne/operativne radnje bile moguće i pod opterećenjem.
- Limit zasnovan na budžetu: Ako se veličine odgovora znatno razlikuju, može pomoći dodatni bajt-budžet (npr. maksimalno X MB istovremeno u generisanju). To je kompleksnije, ali realno za velike preuzimanja.
Važno: Prioritetizacija brzo postane politička („moj Endpoint je važniji“). Tehnički stabilno ostaje ako su prioriteti vezani za procese (npr. unos naloga pre izveštavanja), a ne za uloge ili odeljenja.
Zaključak: Da li se gate isplati — i gde pristup puca?
Concurrency-Gate je pragmatični građevni blok za High Performance REST server u Delphi, jer čini overload kontrolisanim i održava stabilnost vaših sistema pri vršnim opterećenjima. Posebno se isplati kada imate endpoint-e vezane za bazu podataka, kada ispred stoji reverse proxy ili kada više klijenata (Legacy, portali, servisi) generiše opterećenje talasima.
Granice su jasne: ako je stvarni rad po zahtevu preskup (neefikasni upiti, veliki JSON-objekti, blokirajući sistemi trećih strana), gate samo prikriva simptome. Tada treba unaprediti pristup podacima, strategije keširanja, timeoute i po potrebi asinkronu obradu (Queue/Job-System). Kao sigurnosni pojas u radu, gate je često razlika između „kratko trom“ i „potpuno neupotrebljiv“.
Ako želite uvesti ponašanje pri preopterećenju u postojeću Delphi REST-API und REST-Server ili fino izbalansirati limite sa timeout-ovima baze podataka i proxyja: razgovarajte o projektu ili modernizaciji sa Net-Base.
U stručnom kontekstu važnu ulogu igraju i Thread-Pool Delphi i Http 429 Too Many Requests, kada integracije, tokovi podataka i dalji razvoj moraju da se uklope.
Следећи корак
Када тема прерасте у реалан пројекат, архитектуру, постојеће системе и операције треба рано разматрати заједно.
Подржавамо не само у појединачним питањима, већ и када из исечака изворног кода, застарелих тема или идеја за портале треба да настане поуздан корпоративни пројекат.
- Постојеће стање, циљано стање и технички ризици оцењују се заједно.
- REST, приступ подацима, портали и роллаут се неће одлагати као накнадне последице.
- Ви рано видите који пут је економски и оперативно одржив.