Net-Base списание

06.06.2026

Високи перформанси REST сервер во Delphi: ограничувања на барања, пул на нишки и правилно однесување при преоптоварување (фрагмент од изворен код)

High Performance REST сервер во Delphi не е брз само поради „брзо JSON“, туку поради контролирана паралелност, строги Timeouts и чисто однесување при преоптоварување. Овој напис прикажува практично Concurrency-Gate со семафор, 429/503-одговори...

06.06.2026

Од тема во магазинот до проектна пракса

Соодветни страници за услуги и технички информации поврзани со објавата

Зошто „високи перформанси“ кај REST во Delphi често пропаѓаат поради паралелност

Еден сервер со високи перформанси REST Delphi во пракса ретко е ограничен само со CPU-време по барање, туку со неконтролирана паралелност: премногу истовремени барања, премногу истовремени базни податоци-запити или блокирачки I/O (датотека, мрежа, база на податоци). Како резултат тоа не делува како „малку побавно“, туку како синџирна реакција: повеќе тредови, подолги редици за чекање, колапс на connection-pool, зголемени латенции, таймаути на страната на клиентот и на крај сервер што иако уште „жив“, повеќе не дава стабилни одговори.

Противотровата не е поединечен трик, туку свесно поведение при преоптоварување: кога серверот ќе стигне до своите граници, мора рано и детерминистички да одбива (типично HTTP 429 или 503), наместо барањата да ги пушти да чекаат во бесконечна редица. Токму за тоа е наменет овој source-снимок: едно лесно Concurrency-Gate (Semaphore) плус тајмаути, кое може да се интегрира во постоечките REST-endpoints — без разлика дали користите Indy, WebBroker, Horse или сопствен HTTP-слој.

Архитектонска идеја: Concurrency-Gate пред „ресурсоинтензивниот дел“

Основната идеја е едноставна: пред ресурсоинтензивниот дел (пристап до база на податоци, сложени извештаи, големи JSON-одговори) се резервира еден токен од Semaphore. Ако нема слободен токен, веднаш се враќа контролирана одговор. Важно е: ова Gate мора да се ослободува сигурно (try/finally), и мора да биде вградено во патеката на кодот што навистина е ресурсоинтензивна — не само на самиот почеток на request-handler-от, кога потоа сепак следи парсер/рутер/аутиентификација.

На овој начин оптоварувањето не се „исфрла“, туку се канализира: серверот одговара на помалку барања истовремено, но со постабилни латенции. Во индивидуални корпоративни апликации тоа обично е повредно од спорадични рекордни времиња во синтетички бенчмарци.

Фрагмент од изворен код: лимитатор на барања со тајмаут, 429/503 и hooks за телеметрија

Следниот Delphi-код го имплементира Concurrency-Gate како класа TRestRequestGate. Тој се базира на TSemaphore (од System.SyncObjs; една Semaphore е броач за ограничени истовремени пристапи). Повикот на Gate-от враќа или едно „Lease“-објект (слично RAII: ослободување во деструкторот) или одлучува за веднашна одговор на преоптоварување. Дополнително постојат hooks за логирање/мониторинг, за да можете во режим на работа да видите зошто барањата беа одбиени.

Delphi
unit RESTRequestGate;

interface

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

type
  // Минимален контекст за логирање/трасирање; може, на пр., да се прошири за User/Route.
  TRESTGateContext = record
    RequestId: string;
    Route: string;
    RemoteIp: string;
  end;

  TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);

  // Hook за телеметрија на работењето (на пр. во датотека, Syslog, Prometheus-Exporter и слично)
  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-јадра“, туку оперативна големина. Кај endpoint-и со интензивна работа кон база на податоци често е разумно да се постави MaxInFlight во релација со DB-Connection-Pool (на пр. Pool = 20, MaxInFlight = 12 до 16), за да не се случи секој request да блокира конекција и потоа да се влечат дополнителни тредови.

Услови и стапици

  • Try/Finally е задолжително: Lease мора сигурно да се ослободи. Ако имате исклучоци во endpoint-от, иначе вратата ќе „пушти“ и серверот трајно ќе остане на „busy“.
  • Timeout смислено да се избере: TimeoutMs=0 е строг лимит (одбива веднаш). Краток timeout (типично 50 до 150 ms) ги измазнува пикoвите без да формира вистински редици.
  • Gate не премногу рано: Автентикацијата (на пр. Bearer/JWT) или рутирањето може да биде корисно да се изведе пред вратата; семафорот треба да стапи во сила пред вистинскиот скап дел. Обратно: ако автентикацијата е скапа (нпр. против екстерно Identity-System), и таа мора да се ограничува.
  • 429 vs 503: HTTP 429 („Too Many Requests“) е погоден кога клиентите треба да направат целен retry. 503 („Service Unavailable“) е погоден кога услугата временски генерално не е во состојба да прими барања. Во двата случаи се препорачува Retry-After хедер.

Интеграција во REST-Handler: Indy/WebBroker/Horse прагматично

Сниппетот е намерно framework-neutral. Ви треба само едно место каде што requests „прoаѓаат“. Типично е глобален Singleton или едно Gate по група рути (на пр. „/reports“ помало, „/health“ без Gate). Пример за вградување како образец:

  • Пополнување на контекстот (RequestId, Route, RemoteIp)
  • TryAcquire со краток timeout
  • При одбивање веднаш да се запише response (429/503) и да се заврши
  • Lease останува во scope-от до завршувањето на ресурсо-интензивниот дел

Во Horse (Middleware) вратата е поставена блиску до група рути. Во WebBroker можете да работите во соодветниот Action-Handler. Кај Indy тоа зависи дали имате по еден thread за секој request; вратата сепак ќе делува сè додека ресурсно-интензивните делови се јасно ограничени.

High Performance REST Server Delphi: Одговори при преоптоварување кои не ги „загадуваат“ клиентите

Одговорите при преоптоварување значат повеќе од статусни кодови. Ако клиентите при 429/503 агресивно веднаш повторно пратат, ќе добиете бурa на повторни обиди. Во хетерогени системски средини (Mobile Apps, C# Services, Legacy-клиенти) помага доследно однесување:

  • Retry-After: на пр. 1 до 3 секунди, зависно од endpoint. Тоа е јасен такт.
  • Краток body: Мал JSON како {"error":"server_busy","requestId":"..."} е доволен. Големи error-објекти повторно трошат CPU и бендвит.
  • Health-Endpoint без ограничување: Мониторингот треба и при оптоварување да дава информации (евентуално со „degraded“-флаг).

Ако користите reverse proxy како nginx пред апликацијата: синхронизирајте timeouts и buffering таму. Прокси може да ослободи (TLS-Termination, Keep-Alive), но може и да ја префрли оптовареноста (на пр. да буферира големи Request-Bodies). Во производство е важно лимитите да бидат конзистентни: Proxy-Timeout > App-Timeout, иначе клиентите ќе гледаат „Gateway Timeout“, иако апликацијата правилно би одбила.

Threading, DB-Pools und Keep-Alive: каде во пракса доаѓа до проблеми

Das Gate го решава проблемот „преголем број паралелни барања“, но не спречува автоматски едно барање да заземе непропорционално многу ресурси. Три типични точки на кршење од Delphi-проектите настануваат токму на интерфејсите помеѓу Threading, базата на податоци и HTTP-врските:

  • Едно барање блокира повеќе ограничени ресурси: Прво конекција кон DB, потоа надворешен HTTP-повик, па пристап до датотека. Ако сето тоа се случува во истиот Request-Thread, времето на блокада се множи. Gate тогаш го ограничи паралелизмот, но пропусниот опсег драстично опаѓа. Тука има смисла да се одвојат зависностите (на пр. надворешни повици асинхроно, претпресметување преку редица на задачи / Job-Queue).
  • BDE-замена со нативна поврзаност-Pooling und Transaktionen: BDE-Ablosung mit nativer Anbindung може да управува со пул на конекции, но „долга“ трансакција (на пр. затоа што создавањето на JSON или бизнис-проверки се наоѓаат помеѓу StartTransaction и Commit) ја држи конекцијата непотребно. Добра пракса е трансакцијата да се стесни колку што е можно околу самите statements и надвор од трансакцијата да се врши серијализација или валидација, ако тоа е применливо од фахтен аспект.
  • HTTP Keep-Alive како скриен потрошувач на меморија: Keep-Alive ги намалува handshakes-ите, но кај многу неактивни клиенти може да доведе до премногу отворени сокети. Особено кај Windows- и Linux-Services потоа не гледате „CPU високо“, туку „Handles/FDs полни“ или RAM поради баферите. Тука помагаат јасни idle-timeout-ови на серверот и на reverse proxy-то, како и лимит по клиент-IP, ако средината го дозволува.

Последица: MaxInFlight не е статична вредност. Таа зависи од вашата најбавна, најограничена ресурса (DB, надворешни системи, Storage) и од тоа колку добро едно барање ги задржува тие ресурси истовремено.

Лостови за перформанси покрај Gate: JSON, DB и I/O не мешајте

Gate ја стабилизира состојбата, но не ги заменува принципите на економично дизајнирање на endpoints. Три сопирачки во Delphi REST-серверите се појавуваат повторно и повторно:

  • Градење на JSON со непотребни привремени стрингови: Често оптоварување настанува поради многу привремени Unicode-стрингови. Каде што е можно, градејте ориентирано кон стрим (Writer/Stream) наместо големи привремени објекти, особено кај endpoints за листи.
  • Пристап до базата „по елемент“: N+1-queries и per-row lookups се класика. Подобро: таргетирани joins, batch-queries, агрегација на серверот. Кај многу големи резултати, дополнително вреди pagination со стабилна сортирање (за да страниците не „скокаат“).
  • Блокирачки I/O во Request-Thread: Пристапите до датотеки или надворешните HTTP-повици треба или строго да се ограничат или да се преместат во асинхрона pipeline. Инаку ќе ги блокирате скапите тредови со „чекање“.

За зрели дигитални корпоративни решенија тоа често е пресудно: еден endpoint е „брзо додаден“ и работи додека не дојде реално оптоварување и волумен на податоци. Тогаш се покажува дали архитектонските граници се јасно повлечени (слој за пристап до податоци, кеширање, bulk-стратегии, јасни timeout-и).

Дебагирање и оперативна работа: што треба да мерите

Hook-от OnEvent е свесно едноставен. Во практика треба да снимате најмалку следниве вредности:

  • InFlight (тековна паралелност на Gate)
  • WaitedMs (колку „чекирање“/queueing дозволувате)
  • Decision (accepted/busy/timeout)
  • Route/RemoteIp (груба анализа на причини, без да се игнорира заштитата на податоците)
  • Тоа ви дава сигнал дали лимитите се премногу строги (преголем број 429) или премногу благи (високи WaitedMs, растечки латенции). И ќе видите дали поединечни рути доминираат. За Windows- и Linux-Services тоа е решавачко во секојдневната работа: без телеметрија проблемот со перформансите брзо станува игра на среќа помеѓу мрежа, база на податоци, прокси и апликација.

    Необично, но исклучително корисно: „WaitedMs“ како индикатор за рано предупредување

    Многу тимови се фокусираат само на време на одговор и CPU. WaitedMs често е подобар индикатор, бидејќи покажува дека барањата веќе пред самата работа чекаат. Ако WaitedMs расте додека CPU останува умерена, ретката ресурса честопати не е CPU, туку пул (DB-врски), заклучување во бизнис-логиката или надворешен downstream-сервис. Тоа штеди време при анализа на причините, бидејќи ќе барате поцелно кон „Pool/Lock/I/O“ наместо „Compiler-Optimierung“.

    Варијанти: Pro-Route-Gates, приоритети и „Fast Lane“

    Едно Gate за сè е едноставно, но не секогаш идеално. Смислени варијанти:

    • Gate по група на рути: „/reports“ строга, „/api/orders“ умерено, „/health“ отворено. Така спречувате скапи барања за извештаи да ги потиснат клучните процеси.
    • Fast Lane за Admin/Monitoring: Посебно Gate со мала паралелност, за да се овозможат оперативни активности и при оптоварување.
    • Ограничувања базирани на буџет: Кога големините на одговорите значително варираат, дополнително byte-буџет може да помогне (нпр. максимално X MB истовремено во генерирањето). Тоа е покомплексно, но реалистично за големи преземања.

    Важно: приоритизацијата брзо станува политичка („мојот Endpoint е поважен“). Технички стабилно останува ако приоритетите се поврзани со процеси (на пр. прием на нарачки пред извештување), а не со улоги или оддели.

    Заклучок: Дали Gate се исплати — и каде приодот пропаѓа?

    Едно Concurrency-Gate е прагматичен градежен блок за сервер со високи перформанси REST во Delphi, бидејќи го прави преоптоварувањето контролирано и ги одржува вашите системи стабилни при пик-оптоварување. Особено се исплати ако имате endpoints зависни од база на податоци, ако пред нив стои reverse proxy или ако повеќе клиенти (Legacy, портали, Services) во бранови генерираат оптоварување.

    Границите се јасни: ако самата работа по барање е премногу скапа (неефикасни Queries, големи JSON-објекти, блокирачки туѓи системи), Gate-от само ги маскира симптомите. Тогаш мора да се подобрат пристапот до податоци, стратегиите за кеширање, Timeouts и евентуално асинхрона обработка (Queue/Job-System). Како безбедносен појас во оперативната работа, Gate-от често е разликата помеѓу „краткотрајно тешко“ и „целосно неупотребливо“.

    Ако сакате да воведете однесување при преоптоварување во постоечка Delphi REST-API und REST-Server или да ги избалансирате лимитите со database- и proxy-Timeouts, разгледајте проект или модернизациски зафат со Net-Base.

    Во стручниот контекст, thread-pool Delphi и Http 429 Too Many Requests исто така играат важна улога кога интеграциите, протокот на податоци и натамошниот развој треба да соработуваат сигурно.

    Разговарајте за проект или модернизациски зафат со Net-Base.

    Следен чекор

    Кога темата ќе прерасне во реален проект, архитектурата, постоечката средина и експлоатацијата треба рано да се разгледаат заедно.

    Не поддржуваме само при поединечни прашања, туку и кога од исечоци од изворен код, legacy-теми или идеи за портали треба да прерасне во робустен корпоративен проект.

    • Постоечката состојба, целната слика и техничките ризици се проценуваат заедно.
    • REST, пристапот до податоци, порталите и Rollout не се одложуваат како подоцнежни последици.
    • Уште рано идентификувате кој пат е економски и оперативно одржлив.

    Сподели објава

    Споделете го овој пост директно.

    LinkedIn, X, XING, Facebook, WhatsApp и е-пошта се веднаш достапни. За Instagram директно подготвуваме линк и краток текст.

    Е-пошта

    Instagram се отвора во нов таб. Линкот и краткиот текст претходно се копираат во меѓуспремникот.