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.

Nächster Schritt

Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.

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

  • Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
  • REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
  • Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.

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

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

LinkedIn, X, XING, Facebook, WhatsApp und E-Mail sind sofort verfügbar. Für Instagram bereiten wir Link und Kurztext direkt vor.

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

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