Provider epoll para o Horse

September 2, 2026 · View on GitHub

O provedor epoll é uma camada de transporte assíncrona e não-bloqueante nativa para Linux, utilizando a API epoll do kernel (via epoll_wait). Assim como o HTTP.sys, ele é nativo e vem integrado diretamente no core do Horse — nenhum pacote externo é necessário. Após as otimizações da Phase 2, ele conta com Thread-Local Buffer Pools para eliminar disputa por locks globais de memória, parseador de cabeçalhos sem alocação dinâmica de objetos na heap, escrita vetorizada do sistema (writev) e transferência de arquivos com cópia zero via kernel (sendfile).

Nos Defines de Compilação do seu projeto: HORSE_PROVIDER_EPOLL.


⚙️ Propriedades e Arquitetura

  • Exclusivo para Linux: Event loop nativo para distribuições Linux (ideal para Ubuntu, Debian, Alpine e containers Docker).
  • Event Loop Assíncrono: Multiplexa milhares de conexões TCP abertas usando um pool estático e fixo de threads de E/S e workers, superando o gargalo de threads bloqueantes do Indy (thread-per-connection).
  • Thread-Local Buffer Pool (threadvar): Cada thread worker recupera buffers de pools locais, eliminando gargalos de concorrência global por alocação de memória na heap.
  • Parseador Zero-Allocation: Cabeçalhos HTTP são processados e mapeados baseando-se em offsets de bytes sobre o buffer cru do socket, eliminando conversões custosas e instanciação de strings.
  • Escrita Vetorizada (writev): Agrupa os buffers de cabeçalho e corpo da resposta para reduzir chamadas de sistema e alternâncias de contexto (context-switch).
  • Transferência Cópia Zero (sendfile): Envia streams de arquivo (TFileStream) diretamente do cache de página do kernel para o socket de rede, eliminando cópias em User-Space.
  • Timeout Ativo de Keep-Alive: Gerencia conexões inativas com eficiência, liberando sockets ociosos por meio de um timeout automático de 5 segundos.
  • Proteção contra Slowloris: Monitora e derruba conexões lentas ou com fluxos de requisição incompletos.
  • Auto-Elevação de Sockets: Eleva automaticamente os limites de File Descriptors (RLIMIT_NOFILE) do processo para 65535 durante a inicialização, garantindo suporte a volumes massivos de clientes concorrentes.
  • Desempenho: Processou até 29.209 req/s com zero falhas sob estresse de 1.000 conexões Keep-Alive simultâneas em benchmarks comparativos.

🐧 Configurações de Limites do Linux

Para suportar milhares de conexões concorrentes sob o Linux, certifique-se de que os limites de descritores de arquivo do sistema operacional estejam configurados corretamente.

Embora o provedor epoll tente elevar esses limites em tempo de execução automaticamente, você pode inspecionar e definir os limites manualmente na sessão do shell:

# Verifica o limite de descritores de arquivo da sessao atual
ulimit -n

# Define temporariamente o limite para 65535 descritores
ulimit -n 65535

🚀 Tuning do Linux para Produção

Para extrair o máximo de desempenho sob carga de trabalho intensa, configure o kernel do Linux com os seguintes parâmetros no arquivo /etc/sysctl.conf:

# Aumenta a fila de conexões TCP pendentes (backlog)
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535

# Permite a reutilização rápida de conexões em estado TIME_WAIT
net.ipv4.tcp_tw_reuse = 1

# Aumenta a faixa de portas efêmeras disponíveis
net.ipv4.ip_local_port_range = 1024 65535

# Aumenta o limite máximo de descritores de arquivos abertos globalmente
fs.file-max = 2097152

Execute sysctl -p para aplicar as configurações de kernel imediatamente.


⚡ Início Rápido (Quick Start)

  1. Adicione HORSE_PROVIDER_EPOLL aos Defines Condicionais do seu projeto (Project Options → Delphi Compiler → Conditional defines).
  2. Escreva o código do seu servidor normalmente:
program MyServer;

{$APPTYPE CONSOLE}

uses
  Horse;

begin
  THorse.Get('/ping',
    procedure(Req: THorseRequest; Res: THorseResponse)
    begin
      Res.Send('pong');
    end);

  // Inicia o reactor do epoll na porta 9095
  THorse.Listen(9095);
end.

O framework resolverá automaticamente a chamada de THorse.Listen para o reactor nativo epoll quando executado em ambientes Linux.

Modos de execução da pipeline (Delphi)

Por padrão, os handlers das rotas são executados em um pool limitado pertencente ao próprio provider epoll. Isso mantém handlers bloqueantes (acesso ao banco de dados, filesystem ou chamadas remotas) fora das threads do event loop sem alterar as configurações globais de TThreadPool.Default do processo Delphi.

A quantidade padrão de workers é oito vezes a quantidade de processadores e a capacidade padrão da fila de requisições pendentes é 2048. Configure esses valores antes de chamar Listen:

uses
  Horse,
  Horse.Provider.Epoll;

begin
  THorseProviderEpoll.PipelineWorkerThreads := 32; // 0 = automático
  THorseProviderEpoll.PipelineQueueCapacity := 1024;
  THorse.Listen(9095);
end.

Aplicações cujos handlers sejam comprovadamente curtos e não bloqueantes podem optar pela execução inline:

THorseProviderEpoll.PipelineMode := epmInline;

O modo inline elimina o despacho para outra thread e pode melhorar throughput e latência de cauda para handlers muito curtos. Em contrapartida, um único handler lento bloqueia seu worker epoll e atrasa todas as conexões atribuídas àquele event loop. Não use o modo inline em handlers que realizem E/S bloqueante. As configurações da pipeline não podem ser alteradas enquanto o provider estiver em execução.

Quando a fila limitada está cheia, o provider fecha a nova conexão saturada em vez de criar mais threads ou perder silenciosamente a requisição. Durante o shutdown, ele deixa de aceitar trabalho e drena as requisições já aceitas pelo pool antes de liberar os contexts das conexões.