Net-Base Magazyn

06.06.2026

Wysokowydajny serwer REST w Delphi: limity żądań, pula wątków i kontrolowane zachowanie przy przeciążeniu (fragment kodu źródłowego)

Serwer High Performance REST w Delphi jest szybki nie tylko dzięki „szybkiemu JSON”, lecz dzięki kontrolowanej równoległości, twardym limitom czasu i przewidywalnemu zachowaniu przy przeciążeniu. Ten artykuł przedstawia praktyczną bramkę współbieżności opartą na semaforze oraz obsługę odpowiedzi HTTP 429/503...

06.06.2026

Od tematu magazynowego do praktyki projektowej

Pasujące strony usługowe i techniczne do artykułu

Dlaczego „High Performance” w REST w Delphi często zawodzi z powodu równoległości

Ein High Performance REST Server Delphi jest w praktyce rzadko ograniczony wyłącznie przez czas CPU na zapytanie, a zamiast tego przez niekontrolowaną równoległość: zbyt wiele jednoczesnych żądań, zbyt wiele jednoczesnych zapytań do bazy danych lub blokujące I/O (pliki, sieć, baza danych). Efekt nie przypomina „trochę wolniej”, lecz reakcji łańcuchowej: więcej wątków, więcej kolejek, kolaps puli połączeń, rosnące opóźnienia, timeouty po stronie klienta, a w końcu serwer, który co prawda nadal „żyje”, lecz nie dostarcza stabilnych odpowiedzi.

Remedium nie jest pojedynczy trik, lecz świadome Overload-Verhalten: gdy serwer osiąga swoje granice, musi odrzucać żądania wcześnie i deterministycznie (typowo HTTP 429 lub 503), zamiast dopuszczać, by żądania rosły w nieskończonej kolejce. Dokładnie do tego służy ten fragment źródła: lekkie Concurrency-Gate (semafor) z timeoutami, które można zintegrować z istniejącymi endpointami REST – niezależnie od tego, czy używają Państwo Indy, WebBroker, Horse czy własnej warstwy HTTP.

Koncepcja architektoniczna: Concurrency-Gate przed „kosztowną częścią”

Podstawowa idea jest prosta: przed kosztowną częścią (dostęp do bazy danych, złożone raporty, duże odpowiedzi JSON) rezerwuje się token z semafora. Jeśli nie ma wolnego tokena, zwracana jest natychmiast kontrolowana odpowiedź. Ważne: ta bramka musi być nienaruszalnie zwolniona (try/finally) i musi znaleźć się w ścieżce kodu, która jest naprawdę kosztowna – nie tylko na samym początku handlera żądania, gdy potem i tak następują parser/router/uwierzytelnianie.

Dzięki temu obciążenie nie jest „wykasowane”, lecz skanalizowane: serwer obsługuje mniej żądań jednocześnie, za to z bardziej stabilnymi opóźnieniami. W indywidualnych aplikacjach korporacyjnych jest to zwykle cenniejsze niż sporadyczne rekordy w syntetycznych benchmarkach.

Fragment źródła: limiter żądań z timeoutem, 429/503 i hookami telemetrii

Poniższy Delphi-kod implementuje Concurrency-Gate jako klasę TRestRequestGate. Opiera się na TSemaphore (z System.SyncObjs; semafor to licznik ograniczonych jednoczesnych dostępów). Wywołanie bramki zwraca albo obiekt „Lease” (podobny do RAII: zwolnienie w destruktorze) albo decyduje się na natychmiastową odpowiedź o przeładowaniu. Dodatkowo dostępne są hooki do logowania/monitoringu, dzięki którym w czasie pracy widzą Państwo, dlaczego żądania były odrzucane.

Delphi
unit RESTRequestGate;

interface

uses
  System.SysUtils,
  System.Classes,
  System.SyncObjs,
  System.Diagnostics;

type
  // Minimalny kontekst dla logowania/śledzenia; można np. rozszerzyć o użytkownika/trasę.
  TRESTGateContext = record
    RequestId: string;
    Route: string;
    RemoteIp: string;
  end;

  TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);

  // Hook dla telemetrii operacyjnej (np. do pliku, Syslog, Prometheus-Exporter, etc.)
  TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
                                         Decision: TRESTOverloadDecision;
                                         WaitedMs: Integer;
                                         InFlight: Integer);

  // Obiekt lease: zwolnienie tokenu w destruktorze.
  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: brak oczekiwania, natychmiast 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;

  // Najpierw zmniejsz licznik, potem zwolnij semafor.
  TInterlocked.Decrement(FInFlightCounter^);
  FSemaphore.Release;
end;

{ TRESTRequestGate }

constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
  inherited Create;
  if AMaxInFlight <= 0 then
    raise EArgumentException.Create('AMaxInFlight musi być > 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 przy TimeoutMs > 0: oczekiwanie, ale z ograniczeniem.
        Decision := odRejectedTimeout;
        Result := False;
        if Assigned(FOnEvent) then
          FOnEvent(Ctx, Decision, WaitedMs, InFlight);
      end;
  else
    begin
      // wrAbandoned/przypadki błędów: konserwatywnie odrzucić
      Decision := odRejectedBusy;
      Result := False;
      if Assigned(FOnEvent) then
        FOnEvent(Ctx, Decision, WaitedMs, InFlight);
    end;
  end;
end;

end.

Cel: stabilność pod obciążeniem zamiast „wszystko naraz”

Za pomocą MaxInFlight definiuje się, ile żądań jednocześnie może wejść w „kosztowną” część. To świadomie nie jest „Anzahl CPU-Kerne”, lecz parametr operacyjny. Przy endpointach obciążających bazę danych często sensowne jest ustawienie MaxInFlight w relacji do DB-Connection-Pool (na przykład Pool = 20, MaxInFlight = 12 bis 16), aby nie każde żądanie blokowało połączenie i nie powodowało przeciągania kolejnych wątków.

Randbedingungen und Stolperfallen

  • Try/Finally ist Pflicht: Lease muss garantiert freigegeben werden. Wenn Sie Exceptions im Endpoint haben, wird sonst das Gate „undicht“ und der Server bleibt dauerhaft auf „busy“.
  • Timeout sinnvoll wählen: TimeoutMs=0 ist ein hartes Limit (sofort abweisen). Ein kurzes Timeout (typisch 50 bis 150 ms) glättet Peaks, ohne echte Warteschlangen aufzubauen.
  • Gate nicht zu früh: Authentifizierung (zum Beispiel Bearer/JWT) oder Routing kann günstig sein; die Semaphore sollte vor dem wirklich teuren Abschnitt greifen. Umgekehrt: Wenn Auth teuer wird (z.B. gegen ein externes Identity-System), muss auch das begrenzt werden.
  • 429 vs 503: HTTP 429 („Too Many Requests“) passt gut, wenn Clients gezielt retryen sollen. 503 („Service Unavailable“) passt, wenn der Dienst temporär generell nicht in der Lage ist, Anfragen sinnvoll anzunehmen. In beiden Fällen ist ein Retry-After-Header empfehlenswert.

Integration in REST-Handler: Indy/WebBroker/Horse pragmatisch

Der Snippet ist absichtlich framework-neutral. Sie brauchen nur einen Ort, an dem Requests „durchlaufen“. Typisch ist ein globales Singleton oder ein Gate pro Route-Gruppe (zum Beispiel „/reports“ kleiner, „/health“ ohne Gate). Beispielhaft die Einbindung als Muster:

  • Kontext füllen (RequestId, Route, RemoteIp)
  • TryAcquire mit kurzem Timeout
  • Bei Ablehnung sofort Response schreiben (429/503) und beenden
  • Lease lebt im Scope bis nach dem teuren Teil

In Horse (Middleware) liegt das Gate nahe an einer Route-Gruppe. In WebBroker können Sie im jeweiligen Action-Handler arbeiten. Bei Indy hängt es davon ab, ob Sie pro Request einen Thread haben; das Gate wirkt trotzdem, solange die teuren Abschnitte sauber begrenzt werden.

High Performance REST Server Delphi: Overload-Antworten, die Clients nicht „vergiften“

Überlast-Antworten sind mehr als Statuscodes. Wenn Clients bei 429/503 aggressiv sofort erneut senden, haben Sie einen Retry-Sturm. In heterogenen Systemlandschaften (Mobile Apps, C# Services, Legacy-Clients) hilft ein konsistentes Verhalten:

  • Retry-After: zum Beispiel 1 bis 3 Sekunden, je nach Endpoint. Das ist ein klarer Taktgeber.
  • Kurzer Body: Ein kleines JSON wie {"error":"server_busy","requestId":"..."} reicht. Große Error-Objekte kosten wieder CPU und Bandbreite.
  • Health-Endpoint ungedrosselt: Monitoring soll auch bei Last noch Aussagen liefern (ggf. mit „degraded“-Flag).

Wenn Sie einen Reverse Proxy wie nginx davor betreiben: Timeouts und Buffering dort abstimmen. Ein Proxy kann entlasten (TLS-Termination, Keep-Alive), aber auch Last verschieben (zum Beispiel große Request-Bodies puffern). Im Betrieb zählt, dass die Limits konsistent sind: Proxy-Timeout > App-Timeout, sonst sehen Clients „Gateway Timeout“, obwohl die App sauber abgewiesen hätte.

Wątkowanie, pule DB i Keep-Alive: gdzie w praktyce dochodzi do punktu krytycznego

Das Gate rozwiązuje problem „zbyt wiele jednocześnie”, ale nie zapobiega automatycznie temu, że pojedyncze żądanie wiąże nadmiernie wiele zasobów. Trzy typowe punkty krytyczne z projektów Delphi powstają dokładnie na styku wątkowania, bazy danych i połączeń HTTP:

  • Żądanie blokuje kilka ograniczonych zasobów: Najpierw połączenie DB, potem zewnętrzne wywołanie HTTP, następnie dostęp do pliku. Jeśli wszystko dzieje się w tym samym wątku żądania, czas blokady się mnoży. Gate ogranicza co prawda równoległość, ale przepustowość spada drastycznie. Warto tu rozdzielić zależności (np. wywołania zewnętrzne asynchronicznie, wstępne obliczenia przez kolejkę zadań).
  • BDE-zastąpienie z natywną integracją-Pooling i Transaktionen: BDE-Ablosung mit nativer Anbindung może poolować Connections, ale „długa” transakcja (np. ponieważ tworzenie JSON lub kontrole biznesowe znajdują się między StartTransaction a Commit) trzyma połączenie niepotrzebnie. Dobrą praktyką jest ograniczenie transakcji jak najściślej do właściwych zapytań i wykonywanie serializacji lub walidacji poza transakcją, jeśli na to pozwalają wymagania fachowe.
  • HTTP Keep-Alive jako ukryty pożeracz pamięci: Keep-Alive redukuje handshaki, ale przy wielu bezczynnych klientach może prowadzić do zbyt wielu otwartych socketów. Szczególnie w Windows- i Linux-usługi widać wtedy nie „wzrost CPU”, lecz „pełne Handles/FDs” lub zwiększone zużycie RAM przez bufory. Pomagają tu jasne idle-timeouty na serwerze i w reverse proxy oraz limit na adres IP klienta, jeśli środowisko na to pozwala.

Konsekwencja: MaxInFlight nie jest wartością statyczną. Zależy od Państwa najwolniejszego, najbardziej ograniczonego zasobu (DB, systemy zewnętrzne, Storage) i od tego, jak bardzo żądanie te zasoby „trzyma” razem.

Dźwignie wydajności obok Gate: nie mieszać JSON, DB i I/O

Gate stabilizuje, ale nie zastępuje przejrzystej ekonomii endpointów. Trzy hamulce w serwerach Delphi REST pojawiają się wielokrotnie:

  • Budowanie JSON z niepotrzebnymi łańcuchami pośrednimi: Często obciążenie powstaje przez wiele tymczasowych Unicode-Stringów. Tam, gdzie to możliwe, budować zorientowanie na streaming (Writer/Stream) zamiast ogromnych obiektów pośrednich, szczególnie przy endpointach zwracających listy.
  • Dostęp do bazy danych „per Item”: N+1-Queries i per-Row Lookups to klasyk. Lepiej: ukierunkowane Joins, Batch-Queries, agregacja po stronie serwera. Przy bardzo dużych wynikach warto dodatkowo paginacja z stabilnym sortowaniem (żeby strony nie „skakały”).
  • Blokujące I/O we wątku żądania: Dostępy do plików lub zewnętrzne wywołania HTTP powinny być albo ściśle ograniczone, albo przeniesione do asynchronicznego potoku. W przeciwnym razie blokują kosztowne wątki na „oczekiwanie”.

Dla rozrośniętych rozwiązań cyfrowych dla przedsiębiorstw jest to często styk krytyczny: endpoint został „szybko” dopisany i działa, dopóki nie pojawią się realne obciążenia i wolumeny danych. Wtedy widać, czy granice architektury zostały wyraźnie wyznaczone (warstwa dostępu do danych, Caching, Bulk-Strategien, jasne Timeouts).

Debugging und Betrieb: Was Sie messen sollten

Der Hook OnEvent jest świadomie prosty. W praktyce należy zbierać przynajmniej następujące wartości:

  • InFlight (aktualna równoległość przy Gate)
  • WaitedMs (ile „Queueing” Państwo dopuszczają)
  • Decision (accepted/busy/timeout)
  • Route/RemoteIp (wstępna analiza przyczyn, nie ignorując ochrony danych)
  • Dzięki temu otrzymują Państwo sygnał, czy limity są zbyt restrykcyjne (zbyt wiele 429) czy zbyt łagodne (wysokie WaitedMs, rosnące opóźnienia). Widać też, czy pojedyncze trasy dominują. Dla Windows- i Linux-Services jest to w codziennej pracy decydujące: bez telemetrii problem z wydajnością szybko staje się grą zgadywania między siecią, bazą danych, proxy i aplikacją.

    Nietypowe, ale wyjątkowo pomocne: „WaitedMs“ jako wskaźnik wczesnego ostrzegania

    Wiele zespołów patrzy tylko na czas odpowiedzi i CPU. WaitedMs jest często lepszym wskaźnikiem, ponieważ pokazuje, że żądania już przed właściwą pracą oczekują. Jeśli WaitedMs rośnie, podczas gdy wykorzystanie CPU pozostaje umiarkowane, to ograniczonym zasobem często nie jest CPU, lecz pula (połączeń do bazy danych), blokada w logice biznesowej lub zewnętrzny serwis downstream. To oszczędza czas przy analizie przyczyn, ponieważ szukają Państwo celniej w kierunku „Pool/Lock/I/O” zamiast „optymalizacji kompilatora”.

    Warianty: bramki dla poszczególnych tras, priorytety i „Fast Lane”

    Jedna bramka dla wszystkiego jest prosta, ale nie zawsze idealna. Praktyczne warianty:

    • Bramka na grupę tras: „/reports” rygorystycznie, „/api/orders” umiarkowanie, „/health” otwarte. Dzięki temu zapobiegają Państwo, że kosztowne żądania raportów wyprą procesy rdzeniowe.
    • Fast Lane dla admin/monitoringu: Oddzielna bramka z niewielką równoległością, tak aby działania operacyjne były możliwe także pod obciążeniem.
    • Limity oparte na budżecie: Jeśli rozmiary odpowiedzi znacznie się różnią, dodatkowo może pomóc budżet bajtowy (np. maksymalnie X MB jednocześnie podczas generowania). To jest bardziej złożone, ale realistyczne przy dużych pobraniach.

    Ważne: priorytetyzacja szybko staje się kwestią polityczną („mój endpoint jest ważniejszy”). Technicznie stabilne pozostaje to, gdy priorytety są powiązane z procesami (np. rejestracja zamówień przed raportowaniem), a nie z rolami czy działami.

    Wniosek: Czy bramka się opłaca — i kiedy podejście zawodzi?

    Bramka ograniczająca współbieżność to pragmatyczny element dla wysokowydajnego REST serwera w Delphi, ponieważ umożliwia kontrolę przeładowania i utrzymuje stabilność systemów przy szczytowych obciążeniach. Szczególnie opłaca się, gdy mają Państwo endpointy zależne od bazy danych, gdy przed serwerem stoi reverse proxy lub gdy kilku klientów (Legacy, portale, serwisy) generuje obciążenie falami.

    Granice są jasne: jeśli właściwa praca na żądanie jest zbyt kosztowna (nieefektywne zapytania, duże obiekty JSON, blokujące systemy zewnętrzne), bramka maskuje tylko objawy. Wtedy trzeba poprawić dostęp do danych, strategie cache’owania, timeouty i ewentualnie asynchroniczne przetwarzanie (Queue/Job-System). Jako pas bezpieczeństwa w eksploatacji bramka jednak często decyduje o różnicy między „trochę ociężałe” a „całkowicie nieużywalne”.

    Jeśli chcą Państwo wprowadzić zachowania przeciążeniowe w istniejącą Delphi REST-API und REST-Server lub chcą Państwo starannie wyważyć limity względem timeoutów bazy danych i proxy: omówić projekt lub przedsięwzięcie modernizacyjne z Net-Base.

    W kontekście merytorycznym istotną rolę odgrywają także pula wątków Delphi oraz kod HTTP 429 (Too Many Requests), gdy integracje, przepływy danych i rozwój muszą ze sobą ściśle współgrać.

    Omówić projekt lub przedsięwzięcie modernizacyjne z Net-Base.

    Następny krok

    Gdy temat stanie się rzeczywistym projektem, architekturę, stan istniejący i eksploatację należy wcześnie rozpatrywać wspólnie.

    Wspieramy nie tylko w pojedynczych zagadnieniach, lecz także wtedy, gdy z fragmentów kodu źródłowego, kwestii związanych z systemami legacy lub koncepcji portalu ma powstać solidny projekt dla przedsiębiorstwa.

    • Stan istniejący, obraz docelowy i ryzyka techniczne są oceniane łącznie.
    • REST, dostęp do danych, portale i Rollout nie są odkładane na później.
    • Wcześnie widzą Państwo, która droga jest ekonomicznie opłacalna i operacyjnie wykonalna.

    Udostępnij wpis

    Udostępnij ten wpis bezpośrednio

    LinkedIn, X, XING, Facebook, WhatsApp i e‑mail są natychmiast dostępne. Dla Instagrama przygotowujemy od razu link i krótki tekst.

    E-mail

    Instagram otwiera się w nowej karcie. Link i krótki tekst są wcześniej kopiowane do schowka.