Net-Base Журнал

06.06.2026

Високопродуктивний REST сервер у Delphi: ліміти запитів, пул потоків та коректна поведінка при перевантаженні (фрагмент вихідного коду)

Високопродуктивний сервер REST у Delphi прискорюється не лише «швидким JSON», а завдяки контрольованій паралельності, жорстким таймаутам і коректній поведінці при перевантаженні. У цьому матеріалі показано практично придатне Concurrency-Gate із семафором, відповідями 429/503...

06.06.2026

Від теми журналу до практики проєкту

Відповідні сторінки послуг і технічні сторінки до публікації

Чому «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.

Delphi
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, коли інтеграції, потоки даних і подальший розвиток повинні працювати злагоджено.

Обговорити проєкт або модернізацію з Net-Base.

Наступний крок

Якщо тема перетворюється на реальний проєкт, архітектуру, наявну інфраструктуру та експлуатацію слід розглядати разом на ранньому етапі.

Ми підтримуємо не лише в окремих питаннях, а й тоді, коли з уривків вихідного коду, питань, пов’язаних із legacy, або ідей порталу має вирости надійний корпоративний проєкт.

  • Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
  • REST, доступ до даних, портали та розгортання не відкладаються на пізніші етапи.
  • Ви завчасно визначаєте, який підхід є економічно та операційно життєздатним.

Поділитися дописом

Поділитися цим дописом безпосередньо

LinkedIn, X, XING, Facebook, WhatsApp та електронна пошта доступні негайно. Для Instagram ми готуємо посилання та короткий текст безпосередньо.

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

Instagram відкривається в новій вкладці. Посилання та короткий текст попередньо копіюються у буфер обміну.