Net-Base Списание

06.06.2026

Високопроизводителен REST сървър в Delphi: лимити на заявки, Thread-Pool и контролирано поведение при претоварване (фрагмент от изходния код)

Високопроизводителен REST сървър в Delphi не става бърз само благодарение на „бърз JSON“, а чрез контролирана паралелност, строги таймаути и предвидимо поведение при претоварване. Тази статия показва практически приложимо Concurrency-Gate със семафор, 429/503-отговори...

06.06.2026

От темата в списанието към проектната практика

Подходящи страници за услуги и технологии към публикацията

Защо „висока производителност“ при REST в Delphi често се проваля заради паралелността

В практиката един високопроизводителен REST сървър Delphi рядко е ограничен единствено от CPU-времето на заявка; по-често ограничава неконтролирана паралелност: твърде много едновременни заявки, твърде много едновременни заявки към базата данни или блокиращи I/O (файлова система, мрежа, база данни). Резултатът не е „малко по-бавен“, а прилича на верижна реакция: повече нишки, повече опашки, колапс на connection-pool, растящи латенции, таймаути на клиента и в крайна сметка сървър, който въпреки че все още „работи“, не дава стабилни отговори.

Противодействието не е единичен трик, а съзнателно поведение при претоварване (Overload-Verhalten): когато сървърът достигне границите си, той трябва рано и детерминирано да отказва (типично HTTP 429 или 503), вместо да допуска заявките да се трупат в безкрайна опашка. Точно за това е предназначен този фрагмент от сорс кода: лек Concurrency-Gate (Semaphore) с Timeouts, който може да се интегрира в съществуващи REST-ендпойнти – независимо дали използвате Indy, WebBroker, Horse или собствена HTTP-слой.

Архитектурна идея: Concurrency-Gate пред „ресурсоемката част“

Основната идея е проста: преди ресурсоемката част (достъп до база данни, сложни отчети, големи JSON-отговори) се резервира токен от семафор. Ако няма свободен токен, се връща незабавен контролирано отказ. Важно е: този gate трябва да бъде надеждно освобождаван (try/finally), и трябва да бъде поставен във възловия кодов път, който наистина е скъп — не само в самото начало на request-handler-а, когато след това все още има парсери/рутер/аутентификация.

По този начин натоварването не се „премахва“, а се канализира: сървърът отговаря на по-малко заявки едновременно, но с по-стабилни латенции. В индивидуални корпоративни приложения това обикновено е по-ценно от спорадични рекордни времена в синтетични бенчмаркове.

Фрагмент от сорс кода: Request-Limiter с Timeout, 429/503 и Telemetrie-Hooks

Следният Delphi-код реализира Concurrency-Gate като клас TRestRequestGate. Той е базиран на TSemaphore (от System.SyncObjs; семафорът е брояч за ограничени едновременни достъпи). Извикването на gate-а връща или едно „Lease“-обект (RAII-ähnlich: Freigabe im Destructor), или решава за незабавен отговор при претоварване. Допълнително има hooks за логване/мониторинг, за да можете в експлоатация да видите защо заявки са били отхвърлени.

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 трябва да бъде > 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.

Цел: Стабилност при натоварване вместо „всичко наведнъж“

С MaxInFlight определяте колко заявки едновременно могат да достигнат до „скъпата част“. Това умишлено не е „брой CPU ядра“, а експлоатационен параметър. При крайни точки с голям товар върху базата данни често е разумно да зададете MaxInFlight в отношение към DB-Connection-Pool-а (например Pool = 20, MaxInFlight = 12 до 16), така че не всяка заявка да блокира връзка и след това да се включват допълнителни нишки.

Гранични условия и капани

  • Try/Finally ist Pflicht: Lease-ът трябва да бъде гарантирано освободен. Ако имате Exceptions в Endpoint-а, Gate-ът в противен случай ще „пропуска“ и сървърът ще остане трайно в състояние „busy“.
  • Изберете подходящ Timeout: TimeoutMs=0 е твърда граница (незабавно отхвърляне). Кратък Timeout (типично 50 до 150 ms) изглажда пиковете, без да изгражда реални опашки.
  • Не пускайте Gate-а твърде рано: Аутентификацията (например Bearer/JWT) или маршрутирането може да е подходящо място; семафорът трябва да сработи преди действително скъпия участък. Обратно: ако Auth е скъпа (напр. срещу външен Identity-System), и тя трябва да бъде ограничена.
  • 429 vs 503: HTTP 429 („Too Many Requests“) е подходящ, когато клиентите трябва да правят целенасочени retry. 503 („Service Unavailable“) е подходящ, когато услугата временно не е в състояние да приема заявки смислено. В двата случая е препоръчителен Retry-After-Header.

Интеграция в REST-Handler: Indy/WebBroker/Horse прагматично

Фрагментът е умишлено framework-неутрален. Нужно е само едно място, където заявките „преминават“. Типично е глобален Singleton или Gate на група маршрути (например „/reports“ с по-малки ограничения, „/health“ без Gate). По-долу примерна схема за вграждане:

  • Попълване на контекста (RequestId, Route, RemoteIp)
  • TryAcquire с кратък Timeout
  • При отказ незабавно запишете Response (429/503) и прекратете
  • Lease остава в обхвата до след скъпата част

В Horse (Middleware) Gate-ът е близо до група маршрути. В WebBroker можете да работите в съответния Action-Handler. При Indy зависи дали имате по една нишка на заявка; Gate-ът действа, стига скъпите участъци да са ясно ограничени.

High Performance REST Server Delphi: Отговори при претоварване, които не „отровяват“ клиентите

Отговорите при претоварване са повече от статус кодове. Ако клиентите при 429/503 агресивно изпращат незабавно повторно, ще получите буря от retry. В хетерогенни системни ландшафти (Mobile Apps, C# услуги, Legacy-Clients) помага консистентно поведение:

  • Retry-After: например 1 до 3 секунди, в зависимост от Endpoint-а. Това е ясен такт.
  • Кратко тяло: Малък JSON като {"error":"server_busy","requestId":"..."} е достатъчен. Големи обекти за грешки отново консумират CPU и пропусквателна способност.
  • Health-Endpoint без дроселиране: Мониторингът трябва да предоставя индикации и при натоварване (при нужда с „degraded“-Flag).

Ако оперирате Reverse Proxy като nginx пред него: Timeouts и Buffering там трябва да са съгласувани. Прокси може да облекчи (TLS-Termination, Keep-Alive), но и да прехвърли натоварване (например буфериране на големи Request-Bodies). В експлоатация е важно лимитите да са консистентни: Proxy-Timeout > App-Timeout, иначе клиентите виждат „Gateway Timeout“, макар че приложението би отхвърлило правилно.

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

Gate решава проблема с „твърде много едновременно“, но не възпрепятства автоматично един отделен Request да ангажира прекомерно много ресурси. Три типични точки на провал от Delphi-проекти възникват точно на интерфейсите между threading, базата данни и HTTP-връзките:

  • Един Request блокира няколко оскъдни ресурса: Първо DB-връзка, после външен HTTP-вик, после достъп до файл. Ако всичко това се случва в един и същ Request-нишка, времената на блокиране се умножават. Gate ограничава паралелизма, но пропускателната способност пада драстично. Тук си струва да се декуплират зависимостите (напр. външни викания асинхронно, предварителни изчисления чрез job-queue).
  • BDE-Ablosung mit nativer Anbindung-Pooling und Transaktionen: BDE-Ablosung mit nativer Anbindung може да пулли връзки, но „дълга“ транзакция (напр. защото създаване на JSON или бизнес-валидации лежат между StartTransaction и Commit) задържа връзката ненужно. Добра практика е транзакцията да обхваща възможно най-кратко само действителните statements, а сериализацията или валидациите да се извършват извън транзакцията, когато това е възможно по бизнес-логика.
  • HTTP Keep-Alive като скрит консуматор на памет: Keep-Alive намалява ръкостисканията, но при много неактивни клиенти може да доведе до твърде много отворени сокети. Особено при Windows- und Linux-Services често не се вижда „висока CPU“, а „Handles/FDs са изчерпани“ или RAM, заета от буфери. Помагат ясни idle-timeout-и на сървъра и на reverse proxy-то, както и лимит на връзките на client-IP, когато средата го позволява.

Заключението: MaxInFlight не е статична стойност. Тя зависи от най-бавния ви, най-оскъдния ресурс (DB, външни системи, сторидж) и от това колко добре един Request „задържа“ тези ресурси заедно.

Performance-Hebel neben dem Gate: JSON, DB und I/O nicht vermischen

Gate стабилизира, но не замества чистата икономика на endpoint-ите. Три спирачки в Delphi REST-сървъри се срещат повторно:

  • JSON-генериране с ненужни междинни низове: Често натоварването идва от множество временни Unicode-низове. Където е възможно, изграждайте потоково (writer/stream) вместо големи междинни обекти, особено при списъчни endpoints.
  • Достъп до базата „на елемент“: N+1-заявки и per-row lookups са класика. По-добре: целеви joins, batch-queries, сървърна агрегация. При много големи резултати допълнително има смисъл от пагинация с устойчива сортираща последователност (за да не „скочат“ страниците).
  • Блокиращ I/O в Request-нишката: Достъпът до файлове или външни HTTP-викания трябва или строго да се лимитира, или да се измести в асинхронна pipeline. Иначе блокирате скъпи нишки за „чакащи“ операции.

За утвърдени дигитални корпоративни решения това често е критично: един endpoint бил „бързо добавен“ и работи, докато не дойде реална натовареност и обем от данни. Тогава става ясно дали архитектурните граници са коректно дефинирани (слой за достъп до данни, кеширане, bulk-стратегии, ясни timeout-и).

Debugging und Betrieb: Was Sie messen sollten

Hook-ът OnEvent е умишлено опростен. На практика трябва поне да записвате следните стойности:

  • InFlight (актуалната паралелност на Gate-а)
  • WaitedMs (колко „опашване“ допуска системата)
  • Decision (accepted/busy/timeout)
  • Route/RemoteIp (груба Ursachenanalyse, ohne Datenschutz zu ignorieren)

Така ще получите сигнал дали лимитите са прекалено строги (твърде много 429) или прекалено меки (високи WaitedMs, нарастващи латентности). И ще видите дали отделни маршрути доминират. За Windows- и Linux-Services това е от решаващо значение в ежедневието: без телеметрия проблем с производителността бързо се превръща в игра на предположения между мрежа, база данни, прокси и приложението.

Ungewöhnlich, aber extrem hilfreich: „WaitedMs“ als Frühwarnindikator

Много екипи гледат само Response-Time и CPU. WaitedMs е често по-добър индикатор, защото показва, че заявките вече преди самата работа чакат. Ако WaitedMs се увеличава, докато CPU остава умерена, ограничен ресурс често не е CPU, а пул (DB-Verbindungen), блокировка в бизнес-логиката или външен downstream-Service. Това спестява време при анализа на причините, тъй като търсите по-целево в посока „Pool/Lock/I/O“ вместо „Compiler-Optimierung“.

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

Едно Gate за всичко е просто, но не винаги идеално. Смислени варианти:

  • Gate pro Route-Gruppe: „/reports“ стриктно, „/api/orders“ умерено, „/health“ отворено. По този начин предотвратявате скъпите заявки за отчети да изместват критичните процеси.
  • Fast Lane für Admin/Monitoring: Отделно Gate с малка паралелност, за да са възможни оперативните действия и при натоварване.
  • Budget-basierte Limits: Когато размерите на отговорите варират значително, допълнителен байт-бюджет може да помогне (напр. максимално X MB едновременно при генериране). Това е по-сложно, но реалистично при големи изтегляния.

Важно: Приоритизацията бързо става политическа („моята Endpoint е по-важна“). Технически стабилно остава, когато приоритетите са свързани с процеси (напр. запис на поръчка преди отчети), а не с роли или отдели.

Fazit: Lohnt sich das Gate – und wo kippt der Ansatz?

Ein Concurrency-Gate е прагматичен компонент за един High Performance REST Server в Delphi, тъй като прави Overload контролируем и държи вашите системи стабилни при пикова натовареност. Особено си струва, ако имате базиран на база данни endpoints, ако пред него стои Reverse Proxy или ако няколко клиента (Legacy, портали, Services) генерират натоварване на вълни.

Границите са ясни: ако самата работа на заявка е твърде скъпа (ineffiziente Queries, големи JSON-обекти, блокиращи Fremdsysteme), Gate-ът само маскира симптомите. Тогава трябва да се прецизират достъпът до данните, кеширащите стратегии, таймаутите и евентуално асинхронната обработка (Queue/Job-System). Като предпазен колан в експлоатация Gate-ът обаче често е разликата между „леко забавяне“ и „напълно неизползваем“.

Ако искате да въведете Overload-Verhalten в съществуваща Delphi REST-API und REST-Server или да балансирате лимити с таймаути на база данни и прокси по чист начин: обсъдете проект или модернизация с Net-Base.

В професионалния контекст също Thread-Pool Delphi и Http 429 Too Many Requests играят важна роля, когато интеграциите, потокът от данни и по-нататъшното развитие трябва да работят съгласувано.

Обсъдете проект или модернизация с Net-Base.

Следваща стъпка

Когато темата прерасне в реален проект, архитектурата, съществуващото състояние и експлоатацията трябва да бъдат разгледани съвместно още в ранна фаза.

Подпомагаме не само при отделни въпроси, но и когато от фрагменти от изходен код, проблеми с наследени системи или идеи за портал трябва да бъде реализиран надежден корпоративен проект.

  • Сегашното състояние, целевото състояние и техническите рискове се оценяват съвместно.
  • REST, достъпът до данни, порталите и разгръщането не се отлагат като по-късни последици.
  • Виждате рано кой път е икономически и експлоатационно жизнеспособен.

Сподели публикацията

Споделете тази публикация директно

LinkedIn, X, XING, Facebook, WhatsApp и имейл са незабавно достъпни. За Instagram ще подготвим връзка и кратък текст.

Електронна поща

Instagram се отваря в нов раздел. Връзката и краткият текст се копират предварително в клипборда.