Від теми журналу до практики проєкту
Відповідні сторінки послуг і технічні сторінки до публікації
Чому «High Performance» у REST в Delphi часто зазнає невдачі через паралельність
Ein High Performance REST Server Delphi ist in der Praxis selten durch reine CPU-Zeit pro Request limitiert, sondern durch unkontrollierte Parallelität: zu viele gleichzeitige Requests, zu viele gleichzeitige Datenbank-Queries oder blockierende I/O (Datei, Netzwerk, Datenbank). Das Ergebnis wirkt dann nicht wie „ein bisschen langsamer“, sondern wie eine Kettenreaktion: mehr Threads, mehr Warteschlangen, Connection-Pool-Kollaps, steigende Latenzen, Timeouts auf Client-Seite und am Ende ein Server, der zwar noch „lebt“, aber keine stabilen Antworten mehr liefert.
Протидія — це не поодинокий прийом: коли сервер досягає своїх меж, він має відхилити запити рано і детерміністично (типово HTTP 429 або 503), замість того щоб залишати запити в нескінченній черзі. Саме для цього призначений цей фрагмент коду: легковаговий Concurrency-Gate (Semaphore) плюс таймаути, який можна інтегрувати в існуючі REST-ендпоінти — незалежно від того, чи ви використовуєте Indy, WebBroker, Horse або власний HTTP-шар.
Architekturidee: Concurrency-Gate vor dem „teuren Teil“
Die Grundidee ist simpel: Vor dem teuren Teil (Datenbankzugriff, komplexe Reports, große JSON-Antworten) wird ein Token aus einer Semaphore reserviert. Ist kein Token frei, gibt es sofort eine kontrollierte Antwort. Wichtig ist: Dieses Gate muss zuverlässig freigegeben werden (try/finally), und es muss in den Codepfad, der wirklich teuer ist – nicht nur ganz am Anfang des Request-Handlers, wenn danach ohnehin noch Parser/Router/Authentifizierung kommt.
Таким чином навантаження не «вирізається», а каналізується: сервер відповідає на менше запитів одночасно, зате з більш стабільними затримками. Для індивідуальних корпоративних застосунків це зазвичай цінніше, ніж епізодичні рекорди в синтетичних бенчмарках.
Source-Schnipsel: Request-Limiter mit Timeout, 429/503 und Telemetrie-Hooks
Der folgende Delphi-Code implementiert ein Concurrency-Gate als Klasse TRestRequestGate. Es basiert auf TSemaphore (aus System.SyncObjs; eine Semaphore ist ein Zähler für begrenzte gleichzeitige Zugriffe). Der Gate-Aufruf liefert entweder ein „Lease“-Objekt (RAII-ähnlich: Freigabe im Destructor) oder entscheidet sich für eine sofortige Überlast-Antwort. Zusätzlich gibt es Hooks für Logging/Monitoring, damit Sie im Betrieb sehen, warum Requests abgewiesen wurden.
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);
// Хук для експлуатаційної телеметрії (наприклад у файл, 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 ви визначаєте, скільки Requests одночасно можуть потрапити в «дорогу» частину. Це свідомо не «кількість ядер CPU», а операційна величина. Для endpoints, навантажених на базу даних, часто розумно встановлювати MaxInFlight у відношенні до DB-Connection-Pool (наприклад Pool = 20, MaxInFlight = 12–16), щоб кожен Request не блокував з’єднання і не призводив до накопичення потоків.
Умови та підводні камені
- Try/Finally обовязковий: Lease має бути гарантовано звільнений. Якщо в Endpoint виникають Exceptions, Gate в іншому випадку «протікатиме» і сервер залишатиметься постійно в стані «busy».
- Timeout вибирайте розумно:
TimeoutMs=0— це жорстке обмеження (відхилити відразу). Короткий Timeout (типово 50–150 ms) згладжує піки, не створюючи реальних черг. - Не ставте Gate надто рано: Аутентифікація (наприклад Bearer/JWT) або Routing може бути дешевою; семафор має спрацьовувати перед дійсно дорогою частиною. Навпаки: якщо Auth дорога (наприклад проти зовнішньої Identity-системи), її також потрібно обмежити.
- 429 vs 503: HTTP 429 («Too Many Requests») підходить, коли клієнти мають цілеспрямовано робити retry. 503 («Service Unavailable») підходить, коли сервіс тимчасово загалом не в змозі приймати запити адекватно. В обох випадках рекомендовано заголовок
Retry-After.
Integration in REST-Handler: Indy/WebBroker/Horse прагматично
Цей сніпет навмисно framework-neutral. Вам потрібне лише місце, через яке Requests «проходять». Типово це глобальний Singleton або Gate на групу маршрутів (наприклад «/reports» менший, «/health» без Gate). Наведено приклад інтеграції як зразок:
- Заповнити контекст (RequestId, Route, RemoteIp)
TryAcquireз коротким Timeout- При відмові негайно сформувати Response (429/503) і завершити
- Lease діє в області до завершення дорогої частини
У Horse (Middleware) Gate розташований близько до групи маршрутів. У WebBroker ви можете працювати в відповідному Action-Handler. У Indy це залежить від того, чи маєте ви на кожен Request окремий потік; Gate усе одно працює, доки дорогі секції чітко обмежені.
High Performance REST Server Delphi: Overload-Antworten, die Clients nicht „vergiften“
Відповіді при перевантаженні — це більше, ніж статусні коди. Якщо клієнти при 429/503 агресивно одразу повторюють запити, виникає retry-шторм. У гетерогенних ландшафтах систем (Mobile Apps, C# Services, Legacy-Clients) допомагає консистентна поведінка:
- Retry-After: наприклад 1–3 секунди, залежно від Endpoint. Це чіткий тактовий сигнал.
- Коротке тіло відповіді: Невеликий JSON, наприклад
{"error":"server_busy","requestId":"..."}, достатній. Великі error-обєкти знову ж таки коштують CPU і пропускної здатності. - Health-Endpoint без дроселювання: Моніторинг має давати інформацію й під навантаженням (за потреби з прапорцем «degraded»).
Якщо перед сервісом працює Reverse Proxy, наприклад nginx: узгодьте там Timeouts і Buffering. Proxy може знімати навантаження (TLS-Termination, Keep-Alive), але також може зсунути навантаження (наприклад буферизуючи великі Request-Bodies). В експлуатації важливо, щоб ліміти були послідовні: Proxy-Timeout > App-Timeout, інакше клієнти бачитимуть «Gateway Timeout», хоча App коректно відхилив би запит.
Потокування, DB-пули і Keep-Alive: де на практиці відбувається перелом
Das Gate вирішує проблему «zu viele gleichzeitig», але не запобігає автоматично тому, що один запит прив’язує надмірно багато ресурсів. Три типові точки перелому з Delphi-проєктів виникають саме на перетинах між потокуванням (Threading), базою даних і HTTP-з’єднаннями:
- Один Request блокує кілька дефіцитних ресурсів: Спочатку підключення до DB, потім зовнішній HTTP-виклик, потім доступ до файлу. Якщо все це відбувається в тому ж потоці запиту, час блокування множиться. Das Gate хоч і обмежує паралельність, але пропускна здатність падає радикально. Варто розв’язати залежності (наприклад, робити зовнішні виклики асинхронно, підготовка через Job-Queue).
- BDE-заміна з нативним підключенням-Пулінг і транзакції: BDE-Ablosung mit nativer Anbindung може пулити з’єднання, але «довга» транзакція (наприклад через формування JSON або бізнес-перевірки між StartTransaction і Commit) тримає з’єднання без потреби. Правильна практика — обмежити транзакцію якомога тісніше навколо фактичних SQL-операцій і, якщо це технічно можливо, виконувати серіалізацію або валідацію поза транзакцією.
- HTTP Keep-Alive як прихований споживач пам’яті: Keep-Alive зменшує handshakes, але при великій кількості неактивних клієнтів може призвести до багатьох відкритих сокетів. Саме у Windows- та Linux-сервіси часто не видно «CPU високо», а бачите «Handles/FDs повні» або зростання RAM через буфери. Допомагають чіткі Idle-Timeouts на сервері та на Reverse Proxy, а також ліміт на одну Client-IP, якщо середовище дозволяє.
Die Konsequenz: MaxInFlight ist kein statischer Wert. Він залежить від вашого найповільнішого, найдефіцитнішого ресурсу (DB, зовнішні системи, Storage) і від того, наскільки запит ці ресурси «утримує».
Performance-Hebel neben dem Gate: JSON, DB und I/O nicht vermischen
Das Gate стабілізує, але не замінює чисту економіку endpoint-ів. Три гальма в Delphi REST-серверах повторно з’являються:
- Формування JSON з непотрібними проміжними рядками: Часто навантаження виникає через багато тимчасових Unicode-рядків. Де можливо, будувати орієнтовано на стрімінг (Writer/Stream) замість великих проміжних об’єктів, особливо для endpoint-ів списків.
- Доступ до бази даних «на елемент»: N+1-Queries та per-Row Lookups — класика. Краще: цільові JOIN-и, Batch-Queries, серверна агрегація. Для дуже великих результатів також доцільна пагінація зі стабільною сортуванням (щоб сторінки не «стриба ли»).
- Блокуючий I/O у потоці запиту: Доступ до файлів або зовнішні HTTP-виклики слід або строго обмежувати, або переносити в асинхронний pipeline. Інакше ви блокуєте дорогі потоки на «очікування».
Для еволюціонованих цифрових корпоративних рішень це часто виявляється критичною точкою: endpoint було «швидко» додано і він працює, поки не приходить реальне навантаження та обсяги даних. Тоді видно, чи чітко проведено межі архітектури (шар доступу до даних, кешування, Bulk-стратегії, чіткі таймаути).
Debugging und Betrieb: Was Sie messen sollten
Хук OnEvent навмисне простий. На практиці слід принаймні збирати такі значення:
- InFlight (поточна паралельність на Gate)
- WaitedMs (скільки «Queueing» ви допускаєте)
- Decision (accepted/busy/timeout)
- Route/RemoteIp (груба діагностика причин, не ігноруючи вимоги до захисту даних)
Це дає вам сигнал, чи занадто жорсткі ліміти (занадто багато 429) або надто м’які (високі WaitedMs, зростаючі затримки). І ви бачите, чи домінують окремі маршрути. Для Windows- та Linux-Services це в повсякденній роботі вирішально: без телеметрії проблема продуктивності швидко перетворюється на вгадування між мережею, базою даних, проксі та застосунком.
Незвично, але вкрай корисно: „WaitedMs“ як попереджувальний індикатор
Багато команд дивляться лише на Response-Time і CPU. WaitedMs часто є кращим індикатором, тому що показує: запити вже чекають до початку фактичної роботи. Якщо WaitedMs зростає, а CPU залишається помірною, то обмеженим ресурсом часто є не CPU, а пул (DB-з’єднання), блокування в бізнес-логіці або зовнішній downstream-сервіс. Це економить час при аналізі причин, бо ви шукаєте цілеспрямовано в напрямку «Pool/Lock/I/O», а не «оптимізація компілятора».
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 важливіший»). Технічно стабільною вона залишається, коли пріоритети прив’язані до процесів (наприклад, приймання замовлень перед звітністю), а не до ролей чи відділів.
Висновок: Чи виправдовує Gate себе — і де підхід дає збій?
Concurrency-Gate — прагматичний будівельний елемент для High Performance REST сервера в Delphi, оскільки він робить перевантаження контрольованим і утримує ваші системи стабільними під піковим навантаженням. Особливо виправданий, якщо у вас є прив’язані до бази даних Endpoints, якщо перед ним стоїть Reverse Proxy або якщо кілька клієнтів (Legacy, портали, сервіси) генерують навантаження хвилями.
Межі зрозумілі: якщо сама робота на запит занадто дорога (неефективні запити, великі JSON-об’єкти, блокуючі зовнішні системи), Gate лише маскує симптоми. Тоді потрібно опрацювати доступ до даних, стратегії кешування, таймаути і, за потреби, асинхронну обробку (Queue/Job-System). Як страховочний ремінь в експлуатації Gate часто є різницею між «трохи гальмує» і «повністю непридатно».
Якщо ви хочете впровадити поведінку при перевантаженні в існуючу Delphi REST-API und REST-Server або акуратно збалансувати ліміти з таймаутами бази даних і проксі: обговоріть проєкт або модернізацію з Net-Base.
У професійному контексті також важливу роль відіграють Thread-Pool Delphi і Http 429 Too Many Requests, коли інтеграції, потоки даних і подальший розвиток повинні працювати злагоджено.
Наступний крок
Якщо тема перетворюється на реальний проєкт, архітектуру, наявну інфраструктуру та експлуатацію слід розглядати разом на ранньому етапі.
Ми підтримуємо не лише в окремих питаннях, а й тоді, коли з уривків вихідного коду, питань, пов’язаних із legacy, або ідей порталу має вирости надійний корпоративний проєкт.
- Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
- REST, доступ до даних, портали та розгортання не відкладаються на пізніші етапи.
- Ви завчасно визначаєте, який підхід є економічно та операційно життєздатним.