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.
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=0ist 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)
TryAcquiremit 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)
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.