Do tema da revista à prática do projeto
Páginas de serviços e técnicas correspondentes ao artigo
Por que „High Performance“ em REST em Delphi frequentemente falha por paralelismo
Um High Performance REST Server Delphi raramente é limitado, na prática, apenas pelo tempo de CPU por requisição; o limitador costuma ser a paralelidade descontrolada: requisições simultâneas demais, consultas ao banco de dados em paralelo demais ou I/O bloqueante (ficheiro, rede, banco de dados). O efeito não é “um pouco mais lento”, mas uma reação em cadeia: mais threads, mais filas, colapso do pool de conexões, aumento das latências, timeouts no lado do cliente e, no fim, um servidor que ainda “vive”, mas já não entrega respostas estáveis.
A contramedida não é um truque isolado, mas um comportamento consciente de comportamento de sobrecarga: quando o servidor atinge seus limites, ele deve rejeitar cedo e de forma determinística (tipicamente HTTP 429 ou 503), em vez de deixar as requisições acumularem numa fila interminável. É exatamente para isso que serve este trecho de código-fonte: um Concurrency-Gate leve (Semaphore) mais timeouts, que pode ser integrado em endpoints REST existentes — independentemente de você usar Indy, WebBroker, Horse ou uma camada HTTP própria.
Ideia de arquitetura: Concurrency-Gate antes da „parte cara“
A ideia básica é simples: antes da parte cara (acesso ao banco de dados, relatórios complexos, grandes respostas JSON) reserva-se um token de uma Semaphore. Se não houver token disponível, retorna-se imediatamente uma resposta controlada. É importante: esse gate deve ser liberado de forma confiável (try/finally), e deve estar no caminho de código que é realmente custoso — não apenas no início do handler da requisição, quando em seguida ainda há parser/router/autenticação.
Assim a carga não é „otimizada para fora“, mas canalizada: o servidor responde a menos requisições simultâneas, porém com latências mais estáveis. Em aplicações empresariais sob medida isso costuma valer mais do que tempos ótimos esporádicos em benchmarks sintéticos.
Trecho de código-fonte: limitador de requisições com timeout, 429/503 e hooks de telemetria
O código Delphi a seguir implementa um Concurrency-Gate como classe TRestRequestGate. Baseia-se em TSemaphore (de System.SyncObjs; uma Semaphore é um contador para acessos concorrentes limitados). A chamada do gate fornece ou um objeto “Lease” (semelhante a RAII: liberação no Destructor) ou opta por uma resposta imediata de sobrecarga. Além disso existem hooks para logging/monitoramento, para que você, em operação, veja por que requisições foram rejeitadas.
unit RESTRequestGate;
interface
uses
System.SysUtils,
System.Classes,
System.SyncObjs,
System.Diagnostics;
type
// Contexto mínimo para logging/tracing; pode, por exemplo, ser estendido com utilizador/rota.
TRESTGateContext = record
RequestId: string;
Route: string;
RemoteIp: string;
end;
TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);
// Hook para telemetria operacional (p.ex. em ficheiro, Syslog, Prometheus-Exporter, etc.)
TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
Decision: TRESTOverloadDecision;
WaitedMs: Integer;
InFlight: Integer);
// Objeto lease: liberação do token no destructor.
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: sem tempo de espera, imediatamente 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;
// Primeiro decrementar o contador, depois libertar o semáforo.
TInterlocked.Decrement(FInFlightCounter^);
FSemaphore.Release;
end;
{ TRESTRequestGate }
constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
inherited Create;
if AMaxInFlight <= 0 then
raise EArgumentException.Create('AMaxInFlight deve ser > 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 quando TimeoutMs > 0: espera dirigida, mas limitada.
Decision := odRejectedTimeout;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
else
begin
// wrAbandoned/casos de erro: rejeitar de forma conservadora
Decision := odRejectedBusy;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
end;
end;
end.Objetivo: Estabilidade sob carga em vez de „tudo ao mesmo tempo“
Com MaxInFlight você define quantos Requests podem entrar simultaneamente na „parte custosa“. Isso intencionalmente não é „número de núcleos de CPU“, mas uma grandeza operacional. Em endpoints com carga de banco de dados costuma fazer sentido ajustar MaxInFlight em relação ao DB-Connection-Pool (por exemplo Pool = 20, MaxInFlight = 12 bis 16), para que nem todo Request bloqueie uma conexão e então outros threads tenham que aguardar.
Condições e armadilhas
- Try/Finally é obrigatório: Der Lease muss garantiert freigegeben werden. Se você tiver exceções no endpoint, o Gate pode „vazar“ e o servidor permanecerá permanentemente „busy“.
- Escolher timeout adequadamente:
TimeoutMs=0é um limite rígido (rejeita imediatamente). Um timeout curto (típico 50 bis 150 ms) suaviza picos, sem construir filas reais. - Não acionar o Gate cedo demais: Autenticação (por exemplo Bearer/JWT) ou roteamento podem ser baratos; a Semaphore deve atuar antes da seção realmente custosa. Inversamente: se a autenticação for custosa (p.ex. contra um sistema de identidade externo), isso também precisa ser limitado.
- 429 vs 503: HTTP 429 („Too Many Requests“) é apropriado quando os clientes devem fazer retries direcionados. 503 („Service Unavailable“) é adequado quando o serviço, temporariamente, não está em condições de aceitar requisições de forma útil. Em ambos os casos, um cabeçalho
Retry-Afteré recomendável.
Integração em REST-Handler: Indy/WebBroker/Horse pragmático
O Snippet é intencionalmente framework-neutral. Você precisa apenas de um lugar onde as Requests „passam“. Típico é um Singleton global ou um Gate por grupo de rotas (por exemplo „/reports“ menor, „/health“ sem Gate). A seguir um exemplo de integração como padrão:
- Preencher o contexto (RequestId, Route, RemoteIp)
TryAcquirecom timeout curto- Em caso de rejeição, escrever imediatamente a resposta (429/503) e terminar
- Der Lease vive no escopo até depois da parte custosa
No Horse (Middleware) o Gate fica próximo a um grupo de rotas. Em WebBroker você pode atuar no Action-Handler correspondente. No Indy depende de haver um thread por Request; o Gate ainda tem efeito, desde que as seções custosas sejam claramente limitadas.
High Performance REST Server Delphi: Overload-Antworten, die Clients nicht „vergiften“
Respostas de sobrecarga são mais do que códigos de status. Se clientes, ao receber 429/503, reenviarem agressivamente de imediato, você terá uma tempestade de retries. Em ambientes de sistema heterogêneos (Mobile Apps, C# Services, clientes legados) um comportamento consistente ajuda:
- Retry-After: por exemplo 1 bis 3 segundos, dependendo do Endpoint. É um marcador de tempo claro.
- Corpo curto: Um JSON pequeno como
{"error":"server_busy","requestId":"..."}é suficiente. Objetos de erro grandes custam novamente CPU e largura de banda. - Health-Endpoint sem limitação: O monitoramento deve continuar fornecendo informações mesmo sob carga (possivelmente com a flag „degraded“).
Se você operar um Reverse Proxy como nginx na frente: ajuste Timeouts e Buffering ali. Um Proxy pode aliviar (TLS-Termination, Keep-Alive), mas também deslocar carga (por exemplo, bufferizar corpos de Requests grandes). Na operação, o importante é que os limites sejam consistentes: Proxy-Timeout > App-Timeout, caso contrário os clientes podem ver „Gateway Timeout“, embora a App tivesse rejeitado corretamente.
Threading, DB-Pools und Keep-Alive: Wo, na prática, surgem pontos de ruptura
O Gate resolve o problema de “muitos simultâneos”, mas não impede automaticamente que uma única requisição consuma recursos em excesso. Três pontos típicos de ruptura em projetos Delphi surgem exatamente nas interfaces entre Threading, banco de dados e conexões HTTP:
- Uma requisição bloqueia várias recursos escassos: Primeiro uma conexão DB, depois uma chamada HTTP externa, depois um acesso a arquivo. Se tudo isso ocorrer no mesmo thread da requisição, o tempo de bloqueio se multiplica. O Gate limita a paralelidade, mas a taxa de transferência cai drasticamente. Vale a pena desacoplar as dependências (por exemplo, chamadas externas assíncronas, pré-cálculo via fila de jobs).
- BDE-substituição com integração nativa-pooling e transações: BDE-Ablosung mit nativer Anbindung pode manter conexões em pool, mas uma transação “longa” (por exemplo porque a criação de JSON ou verificações de negócio ocorrem entre StartTransaction e Commit) prende a conexão desnecessariamente. Boa prática é manter a transação o mais estreita possível em torno dos statements reais e serializar ou validar fora da transação, quando for possível do ponto de vista funcional.
- HTTP Keep-Alive como consumidor de memória oculto: Keep-Alive reduz handshakes, mas pode, com muitos clientes ociosos, gerar muitos sockets abertos. Especialmente em Windows- e Linux-serviços não se vê “CPU alta”, mas sim “Handles/FDs cheios” ou uso de RAM por buffers. Aqui ajudam timeouts claros de idle no servidor e no reverse proxy, além de um limite por Client-IP, quando o ambiente permitir.
A consequência: MaxInFlight não é um valor estático. Ele depende do seu recurso mais lento e mais escasso (DB, sistemas externos, armazenamento) e de quanto uma requisição “mantém juntos” esses recursos.
Alavancas de performance além do Gate: JSON, DB e I/O não misturar
O Gate estabiliza, mas não substitui uma economia de endpoints bem desenhada. Três freios em servidores Delphi REST-servidores reaparecem com frequência:
- Construção de JSON com strings intermediárias desnecessárias: Frequentemente a carga vem de muitas strings Unicode temporárias. Quando possível, construir orientado a streaming (Writer/Stream) em vez de enormes objetos intermediários, especialmente em endpoints de listagem.
- Acesso ao banco “por item”: N+1-Queries e lookups por linha são o clássico. Melhor: joins direcionados, batch-queries, agregação no servidor. Para resultados muito grandes, vale também a paginação com ordenação estável (para que as páginas não “saltem”).
- I/O bloqueante no thread da requisição: Acessos a arquivos ou chamadas HTTP externas devem ser ou estritamente limitados ou movidos para uma pipeline assíncrona. Caso contrário, você bloqueia threads caros apenas para “espera”.
Para soluções empresariais digitais evoluídas esse costuma ser o ponto crítico: um endpoint foi “adicionado rápido” e funciona até que carga real e volumes de dados apareçam. Então fica evidente se os limites de arquitetura foram bem definidos (camada de acesso a dados, caching, estratégias de bulk, timeouts claros).
Debugging und Betrieb: Was Sie messen sollten
O hook OnEvent é propositalmente simples. Na prática, você deve registrar pelo menos os seguintes valores:
- InFlight (paralelidade atual no Gate)
- WaitedMs (quanto “queueing” você permite)
- Decision (accepted/busy/timeout)
- Route/RemoteIp (análise preliminar das causas, sem negligenciar a proteção de dados)
Isso fornece um sinal sobre se os limites estão demasiado rígidos (muitos 429) ou demasiado permissivos (alto WaitedMs, latências crescentes). E permite ver se rotas individuais estão a dominar. Para Windows- e Linux-Services isto é decisivo no dia a dia: sem telemetria um problema de performance rapidamente torna-se um jogo de adivinhação entre rede, base de dados, proxy e aplicação.
Incomum, mas extremamente útil: „WaitedMs“ como indicador de alerta precoce
Muitas equipas olham apenas para Response-Time e CPU. WaitedMs é frequentemente o indicador mais útil, porque mostra que os pedidos já estão à espera antes do trabalho real começar. Se WaitedMs sobe enquanto a CPU se mantém moderada, o recurso escasso raramente é a CPU — costuma ser um pool (conexões DB), um lock na lógica de negócio ou um serviço downstream externo. Isso poupa tempo na análise de causas, porque permite procurar direcionadamente por “Pool/Lock/I/O” em vez de “otimização do compilador”.
Variante: Gates por rota, prioridades e „Fast Lane“
Um gate único para tudo é simples, mas nem sempre ideal. Variações sensatas:
- Gate por grupo de rotas: “/reports” estrito, “/api/orders” moderado, “/health” aberto. Assim evita-se que requests caros de relatórios desloquem processos centrais.
- Fast Lane para Admin/Monitoring: Gate separado com baixa paralelidade, para que operações de operação sejam possíveis mesmo sob carga.
- Limites baseados em orçamento: Quando os tamanhos de resposta variam muito, um orçamento em bytes adicional pode ajudar (p.ex. máximo de X MB gerados simultaneamente). É mais complexo, mas realista para downloads grandes.
Importante: priorização rapidamente vira questão política (“meu endpoint é mais importante”). Tecnicamente permanece estável se prioridades estiverem acopladas a processos (p.ex. registo de pedidos antes de reporting), não a papéis ou departamentos.
Conclusão: vale a pena o Gate — e quando o método falha?
Um Concurrency-Gate é um componente pragmático para um servidor REST de alta performance em Delphi, pois torna o overload controlável e mantém os sistemas estáveis em picos de carga. Vale especialmente a pena quando tem endpoints ligados a base de dados, quando existe um reverse proxy à frente ou quando múltiplos clientes (legacy, portais, serviços) geram carga em ondas.
Os limites são claros: se o trabalho real por request for demasiado dispendioso (queries ineficientes, grandes objetos JSON, sistemas externos bloqueantes), o Gate apenas disfarça sintomas. Nesse caso é preciso melhorar acesso a dados, estratégias de cache, timeouts e, se necessário, processamento assíncrono (fila/sistema de jobs). Como cinto de segurança na operação, o Gate é muitas vezes a diferença entre “um pouco lento” e “completamente inutilizável”.
Se quiser introduzir comportamento de overload numa existente Delphi REST-API und REST-Server ou equilibrar limites com timeouts de base de dados e proxy de forma consistente: discuta o projeto ou iniciativa de modernização com Net-Base.
No âmbito técnico também têm papel importante Thread-Pool Delphi e HTTP 429 Too Many Requests, quando integrações, fluxos de dados e evolução precisam de funcionar em conjunto de forma controlada.
Discutir projeto ou iniciativa de modernização com Net-Base.
Próximo passo
Quando um tema se torna um projeto real, arquitetura, sistemas existentes e operação devem ser considerados em conjunto desde o início.
Não apenas apoiamos questões pontuais, mas também quando fragmentos de código-fonte, temas legados ou ideias de portais precisam evoluir para um projeto empresarial robusto.
- Estado atual, estado-alvo e riscos técnicos são avaliados em conjunto.
- REST, o acesso a dados, os portais e o Rollout não são adiados para uma fase posterior.
- Você vê cedo qual caminho é economicamente e operacionalmente viável.